From 3a66c0cf74a30288847bcf58adeb255a6d5a8f30 Mon Sep 17 00:00:00 2001 From: nessi Date: Fri, 6 Feb 2026 12:09:21 +0100 Subject: [PATCH] Add `display_name` support for users in backend and frontend This commit introduces the `display_name` field to the user model. It updates database migrations, API endpoints, and the admin panel to handle this field. Additionally, the `display_name` is now shown in the TopBar and WinnerBadge components, improving user experience. --- backend/app/main.py | 9 ++++ backend/app/models.py | 17 +++----- backend/app/routes/admin.py | 38 ++++++++++++++-- backend/app/routes/auth.py | 3 +- frontend/src/components/AdminPanel.jsx | 58 ++++++++++++++++++++++--- frontend/src/components/TopBar.jsx | 4 +- frontend/src/components/WinnerBadge.jsx | 37 +++++++++++++--- 7 files changed, 140 insertions(+), 26 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 2ebf45e..7cf2fb0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -86,6 +86,14 @@ Very small, pragmatic auto-migration (no alembic). - supports old schema (join_code/chip_code) and new schema (code/chip) """ + # --- users.display_name --- + if not _has_column(db, "users", "display_name"): + try: + db.execute(text("ALTER TABLE users ADD COLUMN display_name VARCHAR DEFAULT ''")) + db.commit() + except Exception: + db.rollback() + # --- users.theme_key --- if not _has_column(db, "users", "theme_key"): try: @@ -279,6 +287,7 @@ def ensure_admin(db: Session): password_hash=hash_password(admin_pw), role=Role.admin.value, theme_key="default", + display_name="Admin", ) ) db.commit() diff --git a/backend/app/models.py b/backend/app/models.py index 8508cf7..4c2f286 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -1,3 +1,4 @@ +# backend/app/models.py import enum import uuid from sqlalchemy import ( @@ -34,25 +35,24 @@ class User(Base): disabled: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now()) - # NEW: Theme im Userprofil (damit es auf anderen Geräten mitkommt) theme_key: Mapped[str] = mapped_column(String, default="default") + # NEW: schöner Name für UI (TopBar / WinnerBadge) + display_name: Mapped[str] = mapped_column(String, default="") + class Game(Base): __tablename__ = "games" id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4())) - # NEW: Host (nur Host darf Winner setzen) host_user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), index=True) name: Mapped[str] = mapped_column(String) seed: Mapped[int] = mapped_column(Integer) created_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now()) - # NEW: Join-Code (Kahoot-Style) code: Mapped[str] = mapped_column(String, unique=True, index=True) - # NEW: Winner (aus Users, nicht Freitext) winner_user_id: Mapped[str | None] = mapped_column(String, ForeignKey("users.id"), nullable=True) @@ -75,18 +75,15 @@ class Entry(Base): class SheetState(Base): __tablename__ = "sheet_state" - __table_args__ = ( - UniqueConstraint("game_id", "owner_user_id", "entry_id", name="uq_sheet"), - ) + __table_args__ = (UniqueConstraint("game_id", "owner_user_id", "entry_id", name="uq_sheet"),) id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4())) game_id: Mapped[str] = mapped_column(String, ForeignKey("games.id"), index=True) owner_user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), index=True) entry_id: Mapped[str] = mapped_column(String, ForeignKey("entries.id"), index=True) - status: Mapped[int] = mapped_column(SmallInteger, default=0) # 0 unknown, 1 crossed, 2 confirmed, 3 maybe - note_tag: Mapped[str | None] = mapped_column(String, nullable=True) # null | 'i' | 'm' | 's' + status: Mapped[int] = mapped_column(SmallInteger, default=0) + note_tag: Mapped[str | None] = mapped_column(String, nullable=True) - # NEW: Chip persistieren (statt LocalStorage) chip: Mapped[str | None] = mapped_column(String, nullable=True) \ No newline at end of file diff --git a/backend/app/routes/admin.py b/backend/app/routes/admin.py index b609c14..fbcb723 100644 --- a/backend/app/routes/admin.py +++ b/backend/app/routes/admin.py @@ -19,21 +19,53 @@ def require_admin(req: Request, db: Session) -> User: def list_users(req: Request, db: Session = Depends(get_db)): require_admin(req, db) users = db.query(User).order_by(User.created_at.desc()).all() - return [{"id": u.id, "email": u.email, "role": u.role, "disabled": u.disabled} for u in users] + return [ + { + "id": u.id, + "email": u.email, + "display_name": u.display_name, + "role": u.role, + "disabled": u.disabled, + } + for u in users + ] @router.post("/users") def create_user(req: Request, data: dict, db: Session = Depends(get_db)): require_admin(req, db) email = (data.get("email") or "").lower().strip() password = data.get("password") or "" + display_name = (data.get("display_name") or "").strip() + if not email or not password: raise HTTPException(400, "email/password required") if db.query(User).filter(User.email == email).first(): raise HTTPException(409, "email exists") + role = data.get("role") or Role.user.value if role not in (Role.admin.value, Role.user.value): raise HTTPException(400, "invalid role") - u = User(email=email, password_hash=hash_password(password), role=role) + + u = User(email=email, password_hash=hash_password(password), role=role, display_name=display_name) db.add(u); db.commit() return {"ok": True, "id": u.id} - \ No newline at end of file + +@router.delete("/users/{user_id}") +def delete_user(req: Request, user_id: str, db: Session = Depends(get_db)): + admin = require_admin(req, db) + + if admin.id == user_id: + raise HTTPException(400, "cannot delete yourself") + + u = db.query(User).filter(User.id == user_id).first() + 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 diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index a0ff439..fff2f01 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -40,7 +40,8 @@ def me(req: Request, db: Session = Depends(get_db)): user = db.query(User).filter(User.id == uid).first() if not user: raise HTTPException(status_code=401, detail="not logged in") - return {"id": user.id, "email": user.email, "role": user.role, "theme_key": user.theme_key} + return {"id": user.id, "email": user.email, "role": user.role, "display_name": user.display_name} + @router.patch("/password") diff --git a/frontend/src/components/AdminPanel.jsx b/frontend/src/components/AdminPanel.jsx index a49ac46..29fd712 100644 --- a/frontend/src/components/AdminPanel.jsx +++ b/frontend/src/components/AdminPanel.jsx @@ -7,6 +7,7 @@ export default function AdminPanel() { const [users, setUsers] = useState([]); const [open, setOpen] = useState(false); + const [displayName, setDisplayName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [role, setRole] = useState("user"); @@ -22,6 +23,7 @@ export default function AdminPanel() { }, []); const resetForm = () => { + setDisplayName(""); setEmail(""); setPassword(""); setRole("user"); @@ -32,7 +34,7 @@ export default function AdminPanel() { try { await api("/admin/users", { method: "POST", - body: JSON.stringify({ email, password, role }), + body: JSON.stringify({ display_name: displayName, email, password, role }), }); setMsg("✅ User erstellt."); await loadUsers(); @@ -43,6 +45,16 @@ export default function AdminPanel() { } }; + const deleteUser = async (u) => { + if (!window.confirm(`User wirklich löschen (deaktivieren)?\n\n${u.display_name || u.email}`)) return; + try { + await api(`/admin/users/${u.id}`, { method: "DELETE" }); + await loadUsers(); + } catch (e) { + alert("Fehler: " + (e?.message || "unknown")); + } + }; + const closeModal = () => { setOpen(false); setMsg(""); @@ -63,14 +75,39 @@ export default function AdminPanel() {
{users.map((u) => ( -
-
{u.email}
+
+
+ {u.display_name || "—"} +
+
{u.email}
{u.role}
{u.disabled ? "disabled" : "active"}
+ +
))}
@@ -88,13 +125,21 @@ export default function AdminPanel() {
+ setDisplayName(e.target.value)} + placeholder="Name (z.B. Sascha)" + style={styles.input} + autoFocus + /> + setEmail(e.target.value)} placeholder="Email" style={styles.input} - autoFocus /> + setPassword(e.target.value)} @@ -102,6 +147,7 @@ export default function AdminPanel() { type="password" style={styles.input} /> +