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.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@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}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user