from types import SimpleNamespace from fastapi import APIRouter, Depends, Form, HTTPException, Request from fastapi.responses import RedirectResponse from fastapi_users import exceptions from anode_client import anode_request from auth import ( UserCreate, UserManager, fetch_benutzer_fuer_organisation, get_current_user, get_user_manager, require_org_admin, ) from templating import templates router = APIRouter() 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"): raise HTTPException(status_code=404, detail="Organisation wurde nicht gefunden.") benutzer = fetch_benutzer_fuer_organisation(str(user.organization_id)) 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") def organisation_speichern( name: str = Form(...), kontakt_name: str = Form(default=""), adresse: str = Form(default=""), telefon: str = Form(default=""), kontakt_email: str = Form(default=""), notizen: str = Form(default=""), user=Depends(get_current_user), ): anode_request( "PATCH", f"/api/v1/organizations/{user.organization_id}", json={ "name": name, "kontakt_name": kontakt_name or None, "adresse": adresse or None, "telefon": telefon or None, "kontakt_email": kontakt_email or None, "notizen": notizen or None, }, ) return RedirectResponse("/organisation", status_code=303) @router.post("/organisation/konten") async def konto_anlegen( email: str = Form(...), password: str = Form(...), ist_organisationsadmin: str = Form(default=""), user=Depends(require_org_admin), user_manager: UserManager = Depends(get_user_manager), ): # organization_id kommt bewusst nie aus dem Formular, sondern immer aus # der Session des anfragenden Nutzers - verhindert, dass jemand per # manipuliertem Request ein Konto in einer fremden Organisation anlegt. # is_superuser wird beim Self-Service-Aufruf nie gesetzt (Default False) # - das bleibt exklusiv dem internen Admin-Bereich vorbehalten (siehe # ADR zu is_superuser vs. ist_organisationsadmin). try: await user_manager.create( UserCreate( email=email, password=password, organization_id=user.organization_id, ist_organisationsadmin=ist_organisationsadmin == "on", ) ) except exceptions.InvalidPasswordException as fehler: raise HTTPException(status_code=400, detail=fehler.reason) except exceptions.UserAlreadyExists: raise HTTPException(status_code=400, detail="Ein Konto mit dieser E-Mail-Adresse existiert bereits.") return RedirectResponse("/organisation", status_code=303) @router.post("/organisation/konten/{konto_id}/loeschen") async def konto_loeschen( konto_id: str, user=Depends(require_org_admin), user_manager: UserManager = Depends(get_user_manager), ): if str(user.id) == konto_id: raise HTTPException(status_code=400, detail="Das eigene Konto kann nicht gelöscht werden.") ziel = next( (b for b in fetch_benutzer_fuer_organisation(str(user.organization_id)) if b["id"] == konto_id), None, ) if ziel is None: raise HTTPException(status_code=404, detail="Konto wurde nicht gefunden.") # Über die ID statt per E-Mail nachladen: get_by_email() lief über den # sync-Verbindungspfad von fetch_benutzer_fuer_organisation() zurück in # eine async-Abfrage - psycopg liefert TEXT-Spalten dort teils als bytes # zurück (dasselbe bekannte Verhalten wie bei _row_to_user()), was hier # zu "operator does not exist: text = bytea" führte (live gefunden bei # der Verifikation). user_manager.get(id) nutzt ausschließlich den # async-Pfad (UserDatabase.get(), mit korrektem Decode in _row_to_user()). ziel_user = await user_manager.get(user_manager.parse_id(konto_id)) 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)