#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:
parent
221d9426b7
commit
16f8caedc1
99
app/main.py
99
app/main.py
@ -442,6 +442,105 @@ async def comment_delete(comment_id: int, user: dict = Depends(require_auth)):
|
|||||||
return {"status": "deleted"}
|
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
|
# API: Load assessments from database
|
||||||
@app.get("/api/assessments")
|
@app.get("/api/assessments")
|
||||||
async def list_assessments(bundesland: Optional[str] = None):
|
async def list_assessments(bundesland: Optional[str] = None):
|
||||||
|
|||||||
@ -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>
|
<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;">
|
<hr style="margin:0.3rem 0;border:none;border-top:1px solid #eee;">
|
||||||
<button id="auth-btn" onclick="event.stopPropagation();">🔑 Anmelden</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -966,6 +968,11 @@
|
|||||||
updateAuthUI();
|
updateAuthUI();
|
||||||
loadAssessments(); // Liste neu rendern (Buttons deaktivieren)
|
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
|
// Bestehende Liste neu rendern damit Buttons aktiv werden
|
||||||
if (allAssessments.length > 0) renderList(sortAssessments(allAssessments));
|
if (allAssessments.length > 0) renderList(sortAssessments(allAssessments));
|
||||||
} else {
|
} else {
|
||||||
@ -2362,6 +2369,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Batch-Panel Overlay -->
|
<!-- 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 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;">
|
<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>
|
<h3 style="color:var(--color-blue);margin:0;">📦 Batch-Analyse</h3>
|
||||||
@ -2386,6 +2411,62 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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() {
|
function loadQueuePanel() {
|
||||||
const el = document.getElementById('queue-panel-content');
|
const el = document.getElementById('queue-panel-content');
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user