diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 4c7215b..da49987 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -53,9 +53,11 @@ async def login(req: LoginRequest, request: Request): return { "token": token, "profile_id": prof['id'], - "name": prof['name'], - "role": prof['role'], - "expires_at": expires.isoformat() + "email": prof.get("email"), + "name": prof.get("name"), + "role": prof.get("role"), + "tier": prof.get("tier"), + "expires_at": expires.isoformat(), } diff --git a/backend/routers/profiles.py b/backend/routers/profiles.py index 6bff787..c79b185 100644 --- a/backend/routers/profiles.py +++ b/backend/routers/profiles.py @@ -82,7 +82,12 @@ def get_profile(pid: str, session=Depends(require_auth)): @router.put("/profiles/{pid}") def update_profile(pid: str, p: ProfileUpdate, session=Depends(require_auth)): - """Update profile by ID.""" + """Update profile — nur eigenes Profil oder Admin.""" + sess_pid = session.get('profile_id') + role = (session.get('role') or '').lower() + if str(sess_pid) != str(pid) and role not in ('admin', 'superadmin'): + raise HTTPException(403, 'Keine Berechtigung für dieses Profil') + with get_db() as conn: cur = get_cursor(conn) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) @@ -134,7 +139,7 @@ def update_profile(pid: str, p: ProfileUpdate, session=Depends(require_auth)): if not data: return get_profile(pid, session) - data["updated"] = datetime.now().isoformat() + data["updated_at"] = datetime.now() cols = ", ".join(f"{k}=%s" for k in data) vals = list(data.values()) + [pid] cur.execute(f"UPDATE profiles SET {cols} WHERE id=%s", vals) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 560f396..1f42013 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,7 +13,7 @@ import DesktopSidebar from './components/DesktopSidebar' import { getMainNavItems } from './config/appNav' import LoginPage from './pages/LoginPage' import Dashboard from './pages/Dashboard' -import ProfilePage from './pages/ProfilePage' +import AccountSettingsPage from './pages/AccountSettingsPage' import ExercisesListPage from './pages/ExercisesListPage' import ExerciseDetailPage from './pages/ExerciseDetailPage' import ExerciseFormPage from './pages/ExerciseFormPage' @@ -144,7 +144,8 @@ function AppRoutes() { }> } /> - } /> + } /> + } /> } /> } /> diff --git a/frontend/src/components/DesktopSidebar.jsx b/frontend/src/components/DesktopSidebar.jsx index 4642c43..b95be31 100644 --- a/frontend/src/components/DesktopSidebar.jsx +++ b/frontend/src/components/DesktopSidebar.jsx @@ -61,7 +61,7 @@ export default function DesktopSidebar({ fontSize: '14px' }} > - {user?.name?.charAt(0) || 'U'} + {(user?.name || user?.email || '?').trim().slice(0, 1).toUpperCase()}
diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 89eea51..0015b28 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -29,8 +29,24 @@ export function AuthProvider({ children }) { } } - const login = (data) => { - setUser(data.profile || data) + /** Fallback, falls ohne checkAuth gesetzt wird (Legacy / Token-Injektion) */ + const login = (payload) => { + if (payload?.profile != null) { + setUser(payload.profile) + return + } + const p = payload + if (p?.profile_id != null || p?.id != null) { + setUser({ + id: p.profile_id ?? p.id, + name: p.name ?? null, + email: p.email ?? null, + role: p.role ?? 'user', + tier: p.tier ?? 'free', + }) + return + } + setUser(payload) } const logout = () => { diff --git a/frontend/src/pages/AccountSettingsPage.jsx b/frontend/src/pages/AccountSettingsPage.jsx new file mode 100644 index 0000000..b4044f6 --- /dev/null +++ b/frontend/src/pages/AccountSettingsPage.jsx @@ -0,0 +1,230 @@ +import { useState, useEffect } from 'react' +import { useAuth } from '../context/AuthContext' +import api from '../utils/api' + +/** + * Persönliche Einstellungen (Anzeige/Name, Kontostatus, Passwort). + */ +function AccountSettingsPage() { + const { user, checkAuth } = useAuth() + const [name, setName] = useState('') + const [savingProfile, setSavingProfile] = useState(false) + + const [newPw1, setNewPw1] = useState('') + const [newPw2, setNewPw2] = useState('') + const [savingPw, setSavingPw] = useState(false) + + const [message, setMessage] = useState('') + const [error, setError] = useState('') + + useEffect(() => { + setName(typeof user?.name === 'string' ? user.name : '') + }, [user]) + + const verified = !!user?.email_verified + + const showOk = (text) => { + setMessage(text) + setError('') + setTimeout(() => setMessage(''), 5000) + } + + const showErr = (text) => { + setError(text) + setMessage('') + } + + const handleSaveName = async (e) => { + e.preventDefault() + if (!user?.id) return + const trimmed = (name || '').trim() + if (trimmed.length < 2) { + showErr('Name sollte mindestens 2 Zeichen haben.') + return + } + setSavingProfile(true) + try { + await api.updateProfile(user.id, { name: trimmed }) + await checkAuth() + showOk('Profilname gespeichert.') + } catch (err) { + showErr(err.message || 'Speichern fehlgeschlagen.') + } finally { + setSavingProfile(false) + } + } + + const handleChangePassword = async (e) => { + e.preventDefault() + if (newPw1.length < 4) { + showErr('Neues Passwort: mindestens 4 Zeichen.') + return + } + if (newPw1 !== newPw2) { + showErr('Die Passwörter stimmen nicht überein.') + return + } + setSavingPw(true) + try { + await api.changePassword(newPw1) + setNewPw1('') + setNewPw2('') + showOk('Passwort aktualisiert.') + } catch (err) { + showErr(err.message || 'Passwort konnte nicht geändert werden.') + } finally { + setSavingPw(false) + } + } + + return ( +
+

Einstellungen

+

+ Konto & Sicherheit +

+ + {message && ( +
+ {message} +
+ )} + {error && ( +
+ {error} +
+ )} + +
+

Profil

+
+ E-Mail +
+ {user?.email || '—'}{' '} + {verified ? ( + + bestätigt + + ) : ( + + noch nicht bestätigt + + )} +
+ +
+ + setName(e.target.value)} + placeholder="Dein Name in der App" + autoComplete="nickname" + /> + +
+
+ +
+

Rollen & Tarif

+
+ Rolle + {user?.role === 'admin' ? 'Administrator' : user?.role || 'trainer'} + + Tier + + {user?.tier || 'free'} + +
+
+ +
+

Passwort ändern

+

+ Wähle ein neues Passwort (mindestens 4 Zeichen, wie beim Login gewohnt empfehlen wir längere Passwörter). +

+
+
+ + setNewPw1(e.target.value)} + autoComplete="new-password" + minLength={4} + /> +
+
+ + setNewPw2(e.target.value)} + autoComplete="new-password" + /> +
+ +
+
+
+ ) +} + +export default AccountSettingsPage diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index 8c8effc..5318609 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -13,7 +13,7 @@ function LoginPage() { const [success, setSuccess] = useState('') const navigate = useNavigate() - const { login: authLogin } = useAuth() + const { checkAuth } = useAuth() const handleSubmit = async (e) => { e.preventDefault() @@ -25,7 +25,7 @@ function LoginPage() { if (mode === 'login') { const response = await api.login(email, password) localStorage.setItem('authToken', response.token) - authLogin({ token: response.token, profile: response.profile }) + await checkAuth() navigate('/') } else { await api.register(email, password, name) diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx deleted file mode 100644 index 889c341..0000000 --- a/frontend/src/pages/ProfilePage.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useAuth } from '../context/AuthContext' - -function ProfilePage() { - const { user } = useAuth() - - return ( -
-
-

Profil

- -
-

Persönliche Daten

- -
-
- Name: - {user?.name || '-'} - - E-Mail: - {user?.email} - - Rolle: - - {user?.role || 'user'} - - - Tier: - - {user?.tier || 'free'} - -
-
-
- -
-

Einstellungen

-

- Bearbeitung folgt in Kürze -

-
-
-
- ) -} - -export default ProfilePage diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index b91aa3f..7e5281c 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -93,6 +93,20 @@ export async function getCurrentProfile() { return request('/api/profiles/me') } +export async function updateProfile(profileId, data) { + return request(`/api/profiles/${profileId}`, { + method: 'PUT', + body: JSON.stringify(data), + }) +} + +export async function changePassword(newPassword) { + return request('/api/auth/pin', { + method: 'PUT', + body: JSON.stringify({ pin: newPassword }), + }) +} + // ============================================================================ // Clubs & Groups // ============================================================================ @@ -865,6 +879,8 @@ export const api = { register, logout, getCurrentProfile, + updateProfile, + changePassword, // Clubs & Groups listClubs, diff --git a/frontend/src/version.js b/frontend/src/version.js index 301e0f7..ca6abef 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -6,7 +6,7 @@ export const BUILD_DATE = "2026-04-23" export const PAGE_VERSIONS = { LoginPage: "1.0.0", Dashboard: "1.0.0", - ProfilePage: "1.0.0", + AccountSettingsPage: "1.0.0", ExercisesPage: "1.1.0", // Updated: Katalog-Integration ClubsPage: "1.0.0", SkillsPage: "1.0.0",