kundenplattform/routers/organisation.py
Thomas Stallinger ec11de1ee9 feat: Self-Service-Kontenverwaltung + Organisationsdaten unter /organisation
Neue Rolle ist_organisationsadmin (auth.py), bewusst getrennt von
is_superuser: is_superuser bleibt exklusiv internes, organisationsuebergreifendes
Tuxflotte-Personal mit Zugriff auf /admin/* - ist_organisationsadmin ist
auf die eigene Organisation beschränkt und kann nie über Self-Service
gesetzt werden (organization_id/is_superuser kommen bei
POST /organisation/konten nie aus dem Formular, sondern immer aus der
Session).

/organisation zeigt jetzt Kontakt-/Stammdaten (bearbeitbar) und die
Kolleg:innen-Liste der eigenen Organisation (für alle sichtbar,
Verwaltung nur für Org-Admins/internes Personal via require_org_admin).
Selbstlöschung des eigenen Kontos wird verweigert.

fetch_benutzer_fuer_organisation() von admin_kunden.py nach auth.py
verschoben (jetzt von beiden Routern importiert, kein Duplikat) -
liefert zusätzlich id + ist_organisationsadmin. admin_kunden.py bekommt
eine neue Organisationsdaten-Sektion (nutzt denselben PATCH-Endpoint)
und die neue Checkbox beim Konto-Anlegen, um den ersten Org-Admin einer
neuen Organisation zu bootstrappen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 07:40:05 +02:00

108 lines
3.4 KiB
Python

from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import RedirectResponse
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()
@router.get("/organisation")
def organisation_ansicht(request: Request, user=Depends(get_current_user)):
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 templates.TemplateResponse(
request,
"organisation.html",
{"user": user, "organization": result["organization"], "benutzer": benutzer},
)
@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).
await user_manager.create(
UserCreate(
email=email,
password=password,
organization_id=user.organization_id,
ist_organisationsadmin=ist_organisationsadmin == "on",
)
)
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.")
ziel_user = await user_manager.get_by_email(ziel["email"])
await user_manager.delete(ziel_user)
return RedirectResponse("/organisation", status_code=303)