diff --git a/backend/app/routes/admin.py b/backend/app/routes/admin.py index fbcb723..47e74da 100644 --- a/backend/app/routes/admin.py +++ b/backend/app/routes/admin.py @@ -61,11 +61,48 @@ def delete_user(req: Request, user_id: str, db: Session = Depends(get_db)): if not u: raise HTTPException(404, "not found") - if u.role == Role.admin.value: - raise HTTPException(400, "cannot delete admin user") - # soft delete u.disabled = True db.add(u) db.commit() - return {"ok": True} \ No newline at end of file + return {"ok": True} + + +@router.patch("/users/{user_id}") +def update_user(req: Request, user_id: str, data: dict, db: Session = Depends(get_db)): + admin = require_admin(req, db) + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(404, "not found") + + email = (data.get("email") or user.email).lower().strip() + display_name = (data.get("display_name") if "display_name" in data else user.display_name or "").strip() + role = data.get("role") or user.role + password = data.get("password") or "" + + if not email or "@" not in email: + raise HTTPException(400, "valid email required") + if role not in (Role.admin.value, Role.user.value): + raise HTTPException(400, "invalid role") + if password and len(password) < 8: + raise HTTPException(400, "password too short (min 8)") + if admin.id == user_id and role != Role.admin.value: + raise HTTPException(400, "cannot demote yourself") + if admin.id == user_id and data.get("disabled") is True: + raise HTTPException(400, "cannot disable yourself") + + duplicate = db.query(User).filter(User.email == email, User.id != user_id).first() + if duplicate: + raise HTTPException(409, "email exists") + + user.email = email + user.display_name = display_name + user.role = role + if password: + user.password_hash = hash_password(password) + if "disabled" in data: + user.disabled = bool(data["disabled"]) + + db.add(user) + db.commit() + return {"ok": True} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5f49eba..c5d18b1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -699,7 +699,7 @@ export default function App() { /> {me.role === "admin" && ( - setAdminOpen(false)} /> + setAdminOpen(false)} currentUserId={me.id} /> )} { - if (!dashboardOpen && !userModalOpen) return; - - const prev = document.body.style.overflow; + if (!dashboardOpen && !editorOpen) return; + const previous = document.body.style.overflow; document.body.style.overflow = "hidden"; - - return () => { - document.body.style.overflow = prev; - }; - }, [dashboardOpen, userModalOpen]); + return () => { document.body.style.overflow = previous; }; + }, [dashboardOpen, editorOpen]); useEffect(() => { if (!dashboardOpen) { - setUserModalOpen(false); + setEditorOpen(false); setMsg(""); } }, [dashboardOpen]); - const loadUsers = async () => { - const u = await api("/admin/users"); - setUsers(u); - }; + const loadUsers = async () => setUsers(await api("/admin/users")); - useEffect(() => { - loadUsers().catch(() => {}); - }, []); + useEffect(() => { loadUsers().catch(() => {}); }, []); - const resetForm = () => { - setDisplayName(""); - setEmail(""); - setPassword(""); - setRole("user"); - }; + const setField = (key, value) => setForm((current) => ({ ...current, [key]: value })); - const createUser = async () => { + const openCreate = () => { + setEditingUser(null); + setForm({ ...emptyForm }); setMsg(""); + setEditorOpen(true); + }; + + const openEdit = (user) => { + setEditingUser(user); + setForm({ + displayName: user.display_name || "", + email: user.email || "", + password: "", + role: user.role || "user", + disabled: !!user.disabled, + }); + setMsg(""); + setEditorOpen(true); + }; + + const closeEditor = () => { + setEditorOpen(false); + setEditingUser(null); + setMsg(""); + }; + + const saveUser = async () => { + setMsg(""); + if (!form.email.trim()) return setMsg("❌ E-Mail ist erforderlich."); + if (!editingUser && form.password.length < 8) return setMsg("❌ Passwort muss mindestens 8 Zeichen haben."); + if (editingUser && form.password && form.password.length < 8) return setMsg("❌ Neues Passwort muss mindestens 8 Zeichen haben."); + + setSaving(true); try { - await api("/admin/users", { - method: "POST", - body: JSON.stringify({ display_name: displayName, email, password, role }), + const payload = { + display_name: form.displayName, + email: form.email, + role: form.role, + disabled: form.disabled, + }; + if (form.password) payload.password = form.password; + + await api(editingUser ? `/admin/users/${editingUser.id}` : "/admin/users", { + method: editingUser ? "PATCH" : "POST", + body: JSON.stringify(payload), }); - setMsg("✅ User erstellt."); await loadUsers(); - resetForm(); - setUserModalOpen(false); + closeEditor(); } catch (e) { - setMsg("❌ Fehler: " + (e?.message || "unknown")); + setMsg("❌ " + (e?.message || "Speichern fehlgeschlagen.")); + } finally { + setSaving(false); } }; - const deleteUser = async (u) => { - if (!window.confirm(`User wirklich löschen (deaktivieren)?\n\n${u.display_name || u.email}`)) return; + const disableUser = async (user) => { + if (user.id === currentUserId) return; + if (!window.confirm(`User wirklich deaktivieren?\n\n${user.display_name || user.email}`)) return; try { - await api(`/admin/users/${u.id}`, { method: "DELETE" }); + await api(`/admin/users/${user.id}`, { method: "DELETE" }); await loadUsers(); } catch (e) { alert("Fehler: " + (e?.message || "unknown")); } }; - const closeModal = () => { - setUserModalOpen(false); - setMsg(""); - }; - if (!dashboardOpen) return null; return createPortal(
-
e.stopPropagation()} - > -
-
-
Admin Dashboard
-
- - -
-
- -
- Vorhandene User -
- -
- {users.map((u) => ( -
-
- {u.display_name || "—"} +
e.stopPropagation()}> +
+
+
+
Admin Dashboard
+
Benutzer, Rollen und Zugangsdaten verwalten
-
{u.email}
-
- {u.role} -
-
- {u.disabled ? "disabled" : "active"} -
- - +
- ))} -
- {userModalOpen && - createPortal( -
-
e.stopPropagation()}> -
-
- Neuen User anlegen -
- -
+
+
Vorhandene User ({users.length})
+ +
-
- setDisplayName(e.target.value)} - placeholder="Name (z.B. Sascha)" - style={styles.input} - autoFocus - /> - - setEmail(e.target.value)} - placeholder="Email" - style={styles.input} - /> - - setPassword(e.target.value)} - placeholder="Initial Passwort" - type="password" - style={styles.input} - /> - - - - {msg &&
{msg}
} - -
- - +
- -
- Tipp: Name wird in TopBar & Siegeranzeige genutzt. -
-
-
, - document.body - ) - } + ))} +
+ + {editorOpen && createPortal( +
+
e.stopPropagation()}> +
+
+
{editingUser ? "User bearbeiten" : "Neuen User anlegen"}
+
{editingUser ? "Profil und Zugangsdaten aktualisieren" : "Ein neues Benutzerkonto erstellen"}
+
+ +
+ +
+ + + + + {editingUser && } + {msg &&
{msg}
} + +
+
+
, + document.body + )}
, document.body ); diff --git a/frontend/src/components/HelpModal.jsx b/frontend/src/components/HelpModal.jsx index 328123d..24bf3e7 100644 --- a/frontend/src/components/HelpModal.jsx +++ b/frontend/src/components/HelpModal.jsx @@ -167,7 +167,7 @@ export default function HelpModal({ open, onClose }) {
🛡️ -
Admin Dashboard = nur für Administratoren
+
Admin Dashboard = User anlegen, bearbeiten, Rollen ändern, Passwörter setzen und Konten deaktivieren
diff --git a/frontend/src/styles/hooks/useHpGlobalStyles.js b/frontend/src/styles/hooks/useHpGlobalStyles.js index a8b1c48..2221474 100644 --- a/frontend/src/styles/hooks/useHpGlobalStyles.js +++ b/frontend/src/styles/hooks/useHpGlobalStyles.js @@ -105,8 +105,11 @@ export function useHpGlobalStyles() { @keyframes hpStartLine { 0%, 100% { opacity: .3; transform: scaleX(.7); } 50% { opacity: 1; transform: scaleX(1); } } .hp-topbar-actions { margin-left: auto; } .hp-user-menu-wrap { min-width: 0; } - .hp-admin-user-row { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.5fr) 70px 76px 86px; } + .hp-admin-user-row { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.5fr) 70px 76px 150px; } .hp-admin-action { min-width: 0; } + .hp-admin-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; } + .hp-admin-check { display: flex; align-items: center; gap: 8px; color: var(--hp-textDim); font-size: 13px; } + .hp-admin-check input { width: 18px; height: 18px; accent-color: var(--hp-textGold); } .hp-admin-role { padding: 4px 8px; border-radius: 999px; background: rgba(233,216,166,0.10); border: 1px solid rgba(233,216,166,0.16); font-size: 12px; } .hp-admin-status { color: #baf3c9 !important; font-size: 13px; } .hp-admin-status--disabled { color: #ffb3b3 !important; } @@ -126,7 +129,8 @@ export function useHpGlobalStyles() { .hp-admin-email { grid-column: 1; grid-row: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px !important; } .hp-admin-role { grid-column: 2; grid-row: 1; } .hp-admin-status { grid-column: 2; grid-row: 2; } - .hp-admin-action { grid-column: 2; grid-row: 3; justify-self: end; width: auto; min-width: 94px; min-height: 38px; padding: 7px 14px !important; } + .hp-admin-actions { grid-column: 1 / -1; grid-row: 3; width: 100%; justify-content: flex-end; } + .hp-admin-actions .hp-admin-action { width: auto; min-width: 0; min-height: 38px; padding: 7px 10px !important; } .hp-row { grid-template-columns: minmax(0, 1fr) 42px 62px !important; gap: 7px !important; padding: 12px 11px !important; } .hp-row button { min-height: 40px; } button { min-height: 42px; } diff --git a/frontend/src/styles/styles.js b/frontend/src/styles/styles.js index 570d230..1c7012a 100644 --- a/frontend/src/styles/styles.js +++ b/frontend/src/styles/styles.js @@ -180,6 +180,13 @@ export const styles = { fontWeight: 1000, color: stylesTokens.textGold, }, + adminFieldLabel: { + display: "grid", + gap: 5, + color: stylesTokens.textDim, + fontSize: 12, + fontWeight: 800, + }, userRow: { display: "grid", gridTemplateColumns: "1fr 80px 90px",