From 16f8caedc1ef7da5317496415bbdadb965370db3 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Fri, 10 Apr 2026 23:53:05 +0200 Subject: [PATCH] #103 Registrierung + Admin-Freischaltung + Matrix-Modal-Fix + Issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registrierung: - POST /api/auth/register: erstellt User in Keycloak mit enabled=false - GET /api/auth/pending-users: Liste nicht-freigeschalteter User (Admin) - POST /api/auth/approve-user: User freischalten (Admin) - Registrierungs-Dialog im Hamburger-Menü - Admin: "Freischaltungen"-Button (nur sichtbar mit admin-Rolle) Matrix: - Zeilen-Header klickbar → Erklärung der Berührungsgruppe mit konkretem Lebensalltag-Beispiel - Spalten-Header klickbar → Erklärung des Werts mit Staatsprinzip - Feld-Erklärungen: 25 konkrete Bürger:innen-Texte (Schule, Bus, Miete, Steuer, Spielplatz...) - Spalten nummeriert: "1. Menschenwürde" etc. Neue Issues angelegt: #104 Zeitreihe, #105 Clustering, #106 Abstimmungsverhalten, #107 Vergleichsansicht, #108 Empfehlungen, #109 Share-Buttons --- app/main.py | 99 ++++++++++++++++++++++++++++++++++++++++ app/templates/index.html | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/app/main.py b/app/main.py index f79a9a1..eb5cc7b 100644 --- a/app/main.py +++ b/app/main.py @@ -442,6 +442,105 @@ async def comment_delete(comment_id: int, user: dict = Depends(require_auth)): return {"status": "deleted"} +# ─── Registrierung (#103) ──────────────────────────────────────────────── + +@app.post("/api/auth/register") +async def auth_register( + request: Request, + firstName: str = Form(...), + lastName: str = Form(...), + email: str = Form(...), + username: str = Form(...), + password: str = Form(...), +): + """Registrierung: erstellt User in Keycloak mit enabled=false. + Admin muss den Account manuell freischalten.""" + if len(password) < 8: + raise HTTPException(status_code=400, detail="Passwort muss mindestens 8 Zeichen haben") + + import httpx as _httpx + # Admin-Token holen + async with _httpx.AsyncClient(timeout=10) as client: + token_resp = await client.post( + "https://sso.toppyr.de/realms/master/protocol/openid-connect/token", + data={ + "grant_type": "password", + "client_id": "admin-cli", + "username": "admin", + "password": "J915vI2Ankf7SdmEqe0BC5Aq", + }, + ) + if token_resp.status_code != 200: + raise HTTPException(status_code=500, detail="Keycloak-Verbindung fehlgeschlagen") + admin_token = token_resp.json().get("access_token") + + # User anlegen (disabled) + create_resp = await client.post( + "https://sso.toppyr.de/admin/realms/collaboration/users", + headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"}, + json={ + "username": username, + "email": email, + "firstName": firstName, + "lastName": lastName, + "enabled": False, + "credentials": [{"type": "password", "value": password, "temporary": False}], + }, + ) + if create_resp.status_code == 409: + raise HTTPException(status_code=409, detail="Benutzername oder E-Mail bereits vergeben") + if create_resp.status_code != 201: + raise HTTPException(status_code=500, detail="Registrierung fehlgeschlagen") + + return {"status": "pending_approval", "message": "Registrierung eingegangen. Ein Administrator wird Ihren Account freischalten."} + + +@app.get("/api/auth/pending-users") +async def auth_pending_users(user: dict = Depends(require_admin)): + """Liste nicht-freigeschalteter User (Admin-only).""" + import httpx as _httpx + async with _httpx.AsyncClient(timeout=10) as client: + token_resp = await client.post( + "https://sso.toppyr.de/realms/master/protocol/openid-connect/token", + data={"grant_type": "password", "client_id": "admin-cli", + "username": "admin", "password": "J915vI2Ankf7SdmEqe0BC5Aq"}, + ) + admin_token = token_resp.json().get("access_token") + resp = await client.get( + "https://sso.toppyr.de/admin/realms/collaboration/users?enabled=false&max=50", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + users = resp.json() if resp.status_code == 200 else [] + return [{"id": u["id"], "username": u.get("username"), + "firstName": u.get("firstName"), "lastName": u.get("lastName"), + "email": u.get("email"), "created": u.get("createdTimestamp")} + for u in users] + + +@app.post("/api/auth/approve-user") +async def auth_approve_user( + user_id: str = Form(...), + user: dict = Depends(require_admin), +): + """User freischalten (Admin-only).""" + import httpx as _httpx + async with _httpx.AsyncClient(timeout=10) as client: + token_resp = await client.post( + "https://sso.toppyr.de/realms/master/protocol/openid-connect/token", + data={"grant_type": "password", "client_id": "admin-cli", + "username": "admin", "password": "J915vI2Ankf7SdmEqe0BC5Aq"}, + ) + admin_token = token_resp.json().get("access_token") + resp = await client.put( + f"https://sso.toppyr.de/admin/realms/collaboration/users/{user_id}", + headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"}, + json={"enabled": True}, + ) + if resp.status_code == 204: + return {"status": "approved", "user_id": user_id} + raise HTTPException(status_code=500, detail="Freischaltung fehlgeschlagen") + + # API: Load assessments from database @app.get("/api/assessments") async def list_assessments(bundesland: Optional[str] = None): diff --git a/app/templates/index.html b/app/templates/index.html index 0b916d8..630052f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -744,6 +744,8 @@
+ + @@ -966,6 +968,11 @@ updateAuthUI(); loadAssessments(); // Liste neu rendern (Buttons deaktivieren) }; + // Admin-Features anzeigen + if (currentUser.roles && currentUser.roles.includes('admin')) { + const pendingBtn = document.getElementById('admin-pending-btn'); + if (pendingBtn) pendingBtn.style.display = 'block'; + } // Bestehende Liste neu rendern damit Buttons aktiv werden if (allAssessments.length > 0) renderList(sortAssessments(allAssessments)); } else { @@ -2362,6 +2369,24 @@ + + +