diff --git a/app/main.py b/app/main.py index 4c86553..b8a28e8 100644 --- a/app/main.py +++ b/app/main.py @@ -16,7 +16,7 @@ from app.core.security import create_access_token, hash_password, verify_passwor from app.db.session import Base, engine, get_db from app.models.system import SendLog from app.models.user import User -from app.schemas.user import UserCreate +from app.schemas.user import PasswordChange, ProfileUpdate, UserCreate from app.services.auth import get_admin_user, get_current_user, get_optional_user, require_editor_or_admin from app.services.config_store import get_config, set_config from app.services.csrf import CSRF_COOKIE_NAME, ensure_csrf_cookie, generate_csrf_token, validate_csrf @@ -123,6 +123,8 @@ def _base_context(request: Request, user: User, db: Session) -> dict: active_nav = "dashboard" if path.startswith("/admin/users"): active_nav = "users" + elif path.startswith("/profil"): + active_nav = "profile" return { "user": user, "csrf_token": ensure_csrf_cookie(request), @@ -533,3 +535,110 @@ def create_user( db.add(user) db.commit() return RedirectResponse(url="/admin/users?created=1", status_code=status.HTTP_302_FOUND) + + +def _profile_context(request: Request, user: User, db: Session, **extra) -> dict: + context = _base_context(request, user, db) + context.setdefault("error_message", None) + context.setdefault("pw_error_message", None) + context.setdefault("success_message", None) + context.update(extra) + return context + + +@app.get("/profil", response_class=HTMLResponse) +def profile_page(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + success = None + if request.query_params.get("updated") == "1": + success = "Profil wurde aktualisiert." + elif request.query_params.get("pw") == "1": + success = "Passwort wurde geändert." + return _render(request, "profile.html", _profile_context(request, current_user, db, success_message=success)) + + +@app.post("/profil", response_class=HTMLResponse) +def update_profile( + request: Request, + csrf_token: str = Form(...), + email: str = Form(...), + full_name: str = Form(...), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + try: + validate_csrf(request, csrf_token) + except HTTPException: + return _render( + request, + "profile.html", + _profile_context(request, current_user, db, error_message="Sitzung abgelaufen. Bitte Seite neu laden und erneut versuchen."), + ) + + try: + payload = ProfileUpdate(email=email.strip(), full_name=full_name.strip()) + except ValidationError as exc: + return _render( + request, + "profile.html", + _profile_context(request, current_user, db, error_message=_format_validation_error(exc)), + ) + + clash = db.query(User).filter(User.email == payload.email, User.id != current_user.id).first() + if clash: + return _render( + request, + "profile.html", + _profile_context(request, current_user, db, error_message="Diese E-Mail-Adresse wird bereits verwendet."), + ) + + current_user.email = payload.email + current_user.full_name = payload.full_name + db.commit() + return RedirectResponse(url="/profil?updated=1", status_code=status.HTTP_302_FOUND) + + +@app.post("/profil/passwort", response_class=HTMLResponse) +def change_password( + request: Request, + csrf_token: str = Form(...), + current_password: str = Form(...), + new_password: str = Form(...), + confirm_password: str = Form(...), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + try: + validate_csrf(request, csrf_token) + except HTTPException: + return _render( + request, + "profile.html", + _profile_context(request, current_user, db, pw_error_message="Sitzung abgelaufen. Bitte Seite neu laden und erneut versuchen."), + ) + + if not verify_password(current_password, current_user.password_hash): + return _render( + request, + "profile.html", + _profile_context(request, current_user, db, pw_error_message="Das aktuelle Passwort ist nicht korrekt."), + ) + + if new_password != confirm_password: + return _render( + request, + "profile.html", + _profile_context(request, current_user, db, pw_error_message="Die neuen Passwörter stimmen nicht überein."), + ) + + try: + payload = PasswordChange(new_password=new_password) + except ValidationError as exc: + return _render( + request, + "profile.html", + _profile_context(request, current_user, db, pw_error_message=_format_validation_error(exc)), + ) + + current_user.password_hash = hash_password(payload.new_password) + db.commit() + return RedirectResponse(url="/profil?pw=1", status_code=status.HTTP_302_FOUND) diff --git a/app/schemas/user.py b/app/schemas/user.py index e98152b..1ee9f9d 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -8,6 +8,15 @@ class UserCreate(BaseModel): role: str = Field(default="reader", pattern="^(admin|editor|reader)$") +class ProfileUpdate(BaseModel): + email: EmailStr + full_name: str = Field(min_length=2, max_length=255) + + +class PasswordChange(BaseModel): + new_password: str = Field(min_length=10, max_length=255) + + class UserOut(BaseModel): id: int email: EmailStr diff --git a/app/static/css/main.css b/app/static/css/main.css index 77cca53..1cbdcd3 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -115,6 +115,15 @@ a:hover { color: var(--tk-orange-dark); text-decoration: underline; } color: #CCCCCC; } +a.user-chip-link { + text-decoration: none; + cursor: pointer; +} + +a.user-chip-link:hover { + color: #FFFFFF; +} + .topbar .btn-logout { background: transparent; border: 1px solid rgba(255, 255, 255, 0.35); diff --git a/app/templates/base.html b/app/templates/base.html index 7e310f8..3cc59ac 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -4,7 +4,7 @@ {% block title %}{{ title if title else "Wiki Newsletter Admin" }}{% endblock %} · Thomas-Krenn.AG - +
@@ -22,11 +22,12 @@ {% if user.role == "admin" %} Benutzer {% endif %} + Profil -
+ {{ user.full_name }} {{ user.role }} -
+
diff --git a/app/templates/profile.html b/app/templates/profile.html new file mode 100644 index 0000000..481dd94 --- /dev/null +++ b/app/templates/profile.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} +{% block title %}Mein Profil{% endblock %} +{% block content %} + + + +{% if success_message %} +

{{ success_message }}

+{% endif %} + +
+
+
+
+

Profil bearbeiten

+
+ {% if error_message %} +

{{ error_message }}

+ {% endif %} + + +
+ + +
+ + +
+ +
+
+

Passwort ändern

+
+ {% if pw_error_message %} +

{{ pw_error_message }}

+ {% endif %} +
+ + +
+ + +
+ +
+
+
+ + +
+ +{% endblock %}