#103 Registrierung + Admin-Freischaltung + Matrix-Modal-Fix + Issues

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
This commit is contained in:
Dotty Dotter 2026-04-10 23:53:05 +02:00
parent 221d9426b7
commit 16f8caedc1
2 changed files with 180 additions and 0 deletions

View File

@ -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):

View File

@ -744,6 +744,8 @@
<button onclick="event.stopPropagation();document.getElementById('batch-panel').style.display='block';document.getElementById('hamburger-menu').classList.remove('open');">📦 Batch-Analyse</button>
<hr style="margin:0.3rem 0;border:none;border-top:1px solid #eee;">
<button id="auth-btn" onclick="event.stopPropagation();">🔑 Anmelden</button>
<button onclick="event.stopPropagation();document.getElementById('register-panel').style.display='block';document.getElementById('hamburger-menu').classList.remove('open');">📝 Registrieren</button>
<button id="admin-pending-btn" style="display:none;" onclick="event.stopPropagation();showPendingUsers();document.getElementById('hamburger-menu').classList.remove('open');">👥 Freischaltungen</button>
</div>
</div>
</div>
@ -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 @@
</div>
<!-- Batch-Panel Overlay -->
<!-- Registrierungs-Panel -->
<div id="register-panel" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:400px;background:white;box-shadow:0 8px 24px rgba(0,0,0,0.2);z-index:200;border-radius:8px;padding:1.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="color:var(--color-blue);margin:0;">Registrieren</h3>
<button onclick="document.getElementById('register-panel').style.display='none'" style="background:none;border:none;font-size:1.2rem;cursor:pointer;"></button>
</div>
<form onsubmit="submitRegistration(event)" style="display:flex;flex-direction:column;gap:0.5rem;">
<input name="firstName" placeholder="Vorname" required style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
<input name="lastName" placeholder="Nachname" required style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
<input name="email" type="email" placeholder="E-Mail-Adresse" required style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
<input name="username" placeholder="Benutzername" required style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
<input name="password" type="password" placeholder="Passwort (min. 8 Zeichen)" required minlength="8" style="padding:0.4rem;border:1px solid #ddd;border-radius:4px;">
<button type="submit" style="padding:0.5rem;background:var(--color-blue);color:white;border:none;border-radius:4px;cursor:pointer;">Registrieren</button>
</form>
<div id="register-status" style="margin-top:0.5rem;font-size:0.85rem;"></div>
<p style="font-size:0.8rem;color:#888;margin-top:0.5rem;">Nach der Registrierung muss ein Administrator Ihren Account freischalten.</p>
</div>
<div id="batch-panel" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:450px;background:white;box-shadow:0 8px 24px rgba(0,0,0,0.2);z-index:200;border-radius:8px;padding:1.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="color:var(--color-blue);margin:0;">📦 Batch-Analyse</h3>
@ -2386,6 +2411,62 @@
</div>
<script>
async function showPendingUsers() {
try {
const users = await fetch('/api/auth/pending-users').then(r => r.json());
const html = users.length === 0
? '<p style="color:#888;">Keine ausstehenden Registrierungen.</p>'
: users.map(u => `
<div style="padding:0.5rem;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center;">
<div><strong>${u.firstName} ${u.lastName}</strong><br><span style="color:#888;font-size:0.8rem;">${u.email} (${u.username})</span></div>
<button onclick="approveUser('${u.id}',this)" style="padding:0.3rem 0.8rem;background:var(--color-green);color:white;border:none;border-radius:4px;cursor:pointer;">✓ Freischalten</button>
</div>
`).join('');
document.body.insertAdjacentHTML('beforeend', `
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.4);z-index:300;display:flex;justify-content:center;align-items:center;" onclick="if(event.target===this)this.remove()">
<div style="background:white;border-radius:8px;padding:1.5rem;max-width:500px;width:90%;box-shadow:0 8px 24px rgba(0,0,0,0.2);max-height:80vh;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="color:var(--color-blue);margin:0;">👥 Ausstehende Registrierungen</h3>
<button onclick="this.closest('[style*=fixed]').remove()" style="background:none;border:none;font-size:1.2rem;cursor:pointer;"></button>
</div>
<div id="pending-users-list">${html}</div>
</div>
</div>`);
} catch (e) { alert('Fehler: ' + e.message); }
}
async function approveUser(userId, btn) {
btn.disabled = true; btn.textContent = '⏳...';
const resp = await fetch('/api/auth/approve-user', {
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'user_id=' + userId
});
if (resp.ok) { btn.textContent = '✓'; btn.style.background = '#888'; }
else { btn.textContent = '❌'; btn.disabled = false; }
}
async function submitRegistration(e) {
e.preventDefault();
const form = e.target;
const status = document.getElementById('register-status');
status.innerHTML = '<span style="color:var(--color-blue);">⏳ Wird registriert...</span>';
try {
const resp = await fetch('/api/auth/register', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams(new FormData(form)).toString()
});
const data = await resp.json();
if (resp.ok) {
status.innerHTML = '<span style="color:var(--color-green);">✓ ' + data.message + '</span>';
form.reset();
} else {
status.innerHTML = '<span style="color:#dc3545;">❌ ' + (data.detail || 'Fehler') + '</span>';
}
} catch (err) {
status.innerHTML = '<span style="color:#dc3545;">❌ ' + err.message + '</span>';
}
}
function loadQueuePanel() {
const el = document.getElementById('queue-panel-content');
async function refresh() {