diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index fbd7a55..6ece26a 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response from sqlalchemy.orm import Session from ..db import get_db from ..models import User -from ..security import verify_password, make_session_value, set_session, clear_session, get_session_user_id +from ..security import verify_password, make_session_value, set_session, clear_session, get_session_user_id, hash_password router = APIRouter(prefix="/auth", tags=["auth"]) @@ -30,4 +30,23 @@ def me(req: Request, db: Session = Depends(get_db)): if not user: raise HTTPException(status_code=401, detail="not logged in") return {"id": user.id, "email": user.email, "role": user.role} - \ No newline at end of file + +@router.patch("/password") +def set_password(data: dict, req: Request, db: Session = Depends(get_db)): + uid = get_session_user_id(req) + if not uid: + raise HTTPException(status_code=401, detail="not logged in") + + password = data.get("password") or "" + if len(password) < 8: + raise HTTPException(status_code=400, detail="password too short (min 8)") + + user = db.query(User).filter(User.id == uid, User.disabled == False).first() + if not user: + raise HTTPException(status_code=401, detail="not logged in") + + user.password_hash = hash_password(password) + db.add(user) + db.commit() + + return {"ok": True} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 86b2737..090a6f3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -201,6 +201,15 @@ export default function App() { const [helpOpen, setHelpOpen] = useState(false); + // User dropdown + Passwort Modal + const [userMenuOpen, setUserMenuOpen] = useState(false); + + const [pwOpen, setPwOpen] = useState(false); + const [pw1, setPw1] = useState(""); + const [pw2, setPw2] = useState(""); + const [pwMsg, setPwMsg] = useState(""); + const [pwSaving, setPwSaving] = useState(false); + const load = async () => { const m = await api("/auth/me"); setMe(m); @@ -211,6 +220,18 @@ export default function App() { if (gs[0] && !gameId) setGameId(gs[0].id); }; + // Usermanager + useEffect(() => { + const onDown = (e) => { + // wenn Dropdown offen & Klick ist NICHT in einem Element mit data-user-menu + const root = e.target?.closest?.("[data-user-menu]"); + if (!root) setUserMenuOpen(false); + }; + + if (userMenuOpen) document.addEventListener("mousedown", onDown); + return () => document.removeEventListener("mousedown", onDown); + }, [userMenuOpen]); + // Google Fonts useEffect(() => { if (document.getElementById("hp-fonts")) return; @@ -330,6 +351,52 @@ export default function App() { setSheet(null); }; + // ===== Password change ===== + const openPwModal = () => { + setPwMsg(""); + setPw1(""); + setPw2(""); + setPwOpen(true); + setUserMenuOpen(false); + }; + + const closePwModal = () => { + setPwOpen(false); + setPwMsg(""); + setPw1(""); + setPw2(""); + }; + + const savePassword = async () => { + setPwMsg(""); + + if (!pw1 || pw1.length < 8) { + setPwMsg("❌ Passwort muss mindestens 8 Zeichen haben."); + return; + } + if (pw1 !== pw2) { + setPwMsg("❌ Passwörter stimmen nicht überein."); + return; + } + + setPwSaving(true); + try { + await api("/auth/password", { + method: "PATCH", + body: JSON.stringify({ password: pw1 }), + }); + + setPwMsg("✅ Passwort gespeichert."); + // Optional: nach kurzer Zeit automatisch schließen + setTimeout(() => closePwModal(), 650); + } catch (e) { + // dein api() wirft res.text() -> oft JSON Detail. Wir zeigen es einfach roh. + setPwMsg("❌ Fehler: " + (e?.message || "unknown")); + } finally { + setPwSaving(false); + } + }; + const newGame = async () => { const g = await api("/games", { method: "POST", @@ -568,10 +635,41 @@ export default function App() {