From 1fdac842209a0fffd0e1cd926055965ed79bb4b2 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Wed, 26 Aug 2026 09:48:33 +0200 Subject: [PATCH] =?UTF-8?q?Self-Service:=20Passwort=20=C3=A4ndern=20(POST?= =?UTF-8?q?=20/organisation/passwort)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Sektion 'Mein Konto' auf der bestehenden /organisation-Seite. Jeder eingeloggte Nutzer darf sein eigenes Passwort ändern (kein require_org_admin nötig, anders als bei /organisation/konten für fremde Konten). Prüft aktuelles Passwort per user_manager.authenticate() (gleiches Muster wie der Login), validiert das neue Passwort explizit gegen die bestehende Policy (>=12 Zeichen, nicht gleich der E-Mail) aus der Sicherheitshärtung vom 25.08.2026, zeigt Fehler inline auf derselben Seite statt als nackte HTTPException. Co-Authored-By: Claude Opus 5 --- routers/organisation.py | 70 +++++++++++++++++++++++++++++++++---- templates/organisation.html | 18 ++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/routers/organisation.py b/routers/organisation.py index 0ced976..1891221 100644 --- a/routers/organisation.py +++ b/routers/organisation.py @@ -1,3 +1,5 @@ +from types import SimpleNamespace + from fastapi import APIRouter, Depends, Form, HTTPException, Request from fastapi.responses import RedirectResponse from fastapi_users import exceptions @@ -17,8 +19,14 @@ from templating import templates router = APIRouter() -@router.get("/organisation") -def organisation_ansicht(request: Request, user=Depends(get_current_user)): +def _organisation_kontext(user: dict) -> dict: + """ + Gemeinsamer Seitenkontext für /organisation - wird sowohl vom normalen + GET als auch von passwort_aendern() bei einem Fehler gebraucht (dort + muss die Seite mit allen bestehenden Daten neu gerendert werden, nicht + nur mit der Fehlermeldung). + """ + result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}") if not result.get("success"): @@ -26,11 +34,12 @@ def organisation_ansicht(request: Request, user=Depends(get_current_user)): benutzer = fetch_benutzer_fuer_organisation(str(user.organization_id)) - return templates.TemplateResponse( - request, - "organisation.html", - {"user": user, "organization": result["organization"], "benutzer": benutzer}, - ) + return {"user": user, "organization": result["organization"], "benutzer": benutzer} + + +@router.get("/organisation") +def organisation_ansicht(request: Request, user=Depends(get_current_user)): + return templates.TemplateResponse(request, "organisation.html", _organisation_kontext(user)) @router.post("/organisation") @@ -118,3 +127,50 @@ async def konto_loeschen( await user_manager.delete(ziel_user) return RedirectResponse("/organisation", status_code=303) + + +@router.post("/organisation/passwort") +async def passwort_aendern( + request: Request, + aktuelles_passwort: str = Form(...), + neues_passwort: str = Form(...), + neues_passwort_wiederholung: str = Form(...), + user=Depends(get_current_user), + user_manager: UserManager = Depends(get_user_manager), +): + """ + Self-Service-Passwortänderung für den eingeloggten Nutzer selbst - + anders als /organisation/konten (fremdes Konto anlegen/löschen, nur für + require_org_admin) braucht das hier keine besondere Rolle, jeder + eingeloggte Nutzer darf sein eigenes Passwort ändern. + """ + + fehler = None + + if neues_passwort != neues_passwort_wiederholung: + fehler = "Die Wiederholung stimmt nicht mit dem neuen Passwort überein." + else: + # Gleiches Authenticate()-Muster wie beim Login (auth.py) - prüft + # das aktuelle Passwort, ohne eine eigene Vergleichslogik zu bauen. + authentifiziert = await user_manager.authenticate( + SimpleNamespace(username=user.email, password=aktuelles_passwort) + ) + + if authentifiziert is None: + fehler = "Das aktuelle Passwort ist falsch." + else: + try: + await user_manager.validate_password(neues_passwort, user) + except exceptions.InvalidPasswordException as exc: + fehler = exc.reason + else: + await user_manager.user_db.update( + user, {"hashed_password": user_manager.password_helper.hash(neues_passwort)} + ) + + if fehler: + kontext = _organisation_kontext(user) + kontext["passwort_fehler"] = fehler + return templates.TemplateResponse(request, "organisation.html", kontext, status_code=400) + + return RedirectResponse("/organisation", status_code=303) diff --git a/templates/organisation.html b/templates/organisation.html index 1ab50c4..8c8cb9a 100644 --- a/templates/organisation.html +++ b/templates/organisation.html @@ -25,6 +25,24 @@ +
+

Mein Konto

+ {% if passwort_fehler %}

{{ passwort_fehler }}

{% endif %} +
+ + + + + + Mindestens 12 Zeichen, darf nicht der E-Mail-Adresse entsprechen. + + + + + +
+
+

Kolleg:innen

{% if not benutzer %}