feat: update authentication and profile management features
- Enhanced login response to include additional user information such as email, tier, and role. - Updated profile update logic to restrict access based on user roles and ensure only authorized users can modify profiles. - Replaced ProfilePage with AccountSettingsPage in routing and updated related components to reflect this change. - Added new API functions for updating profiles and changing passwords to improve user account management.
This commit is contained in:
parent
d9d2d9e506
commit
fae673670a
|
|
@ -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(),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
|
||||
<Route element={<ProtectedLayout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="profile" element={<Navigate to="/settings" replace />} />
|
||||
<Route path="settings" element={<AccountSettingsPage />} />
|
||||
<Route path="exercises">
|
||||
<Route index element={<ExercisesListPage />} />
|
||||
<Route path="new" element={<ExerciseFormPage />} />
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default function DesktopSidebar({
|
|||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
{user?.name?.charAt(0) || 'U'}
|
||||
{(user?.name || user?.email || '?').trim().slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<div className="desktop-sidebar__user-text">
|
||||
<span className="desktop-sidebar__user-name">
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
230
frontend/src/pages/AccountSettingsPage.jsx
Normal file
230
frontend/src/pages/AccountSettingsPage.jsx
Normal file
|
|
@ -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 (
|
||||
<div className="page-padding" style={{ padding: '1rem', maxWidth: '640px', margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: '0.35rem', fontSize: '1.5rem' }}>Einstellungen</h1>
|
||||
<p style={{ color: 'var(--text2)', marginBottom: '1.25rem', fontSize: '0.95rem' }}>
|
||||
Konto & Sicherheit
|
||||
</p>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
borderRadius: 'var(--radius, 12px)',
|
||||
background: 'var(--accent-soft, rgba(29,158,117,0.15))',
|
||||
color: 'var(--text1)',
|
||||
marginBottom: '1rem',
|
||||
border: '1px solid var(--accent)',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
borderRadius: 'var(--radius, 12px)',
|
||||
background: 'rgba(216,90,48,0.15)',
|
||||
color: 'var(--text1)',
|
||||
marginBottom: '1rem',
|
||||
border: '1px solid var(--danger)',
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card" style={{ marginBottom: '1rem' }}>
|
||||
<h2 style={{ margin: '0 0 0.75rem', fontSize: '1.1rem' }}>Profil</h2>
|
||||
<div style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem', lineHeight: 1.5 }}>
|
||||
<strong style={{ color: 'var(--text1)' }}>E-Mail</strong>
|
||||
<br />
|
||||
{user?.email || '—'}{' '}
|
||||
{verified ? (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '0.5rem',
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.15rem 0.5rem',
|
||||
borderRadius: '6px',
|
||||
background: 'var(--accent-soft, rgba(29,158,117,0.2))',
|
||||
color: 'var(--accent-dark, #085041)',
|
||||
}}
|
||||
>
|
||||
bestätigt
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: '0.5rem',
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.15rem 0.5rem',
|
||||
borderRadius: '6px',
|
||||
background: 'var(--surface2)',
|
||||
color: 'var(--text2)',
|
||||
}}
|
||||
>
|
||||
noch nicht bestätigt
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveName}>
|
||||
<label className="form-label" htmlFor="settings-name">
|
||||
Anzeigename
|
||||
</label>
|
||||
<input
|
||||
id="settings-name"
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Dein Name in der App"
|
||||
autoComplete="nickname"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={savingProfile}
|
||||
style={{ marginTop: '0.85rem' }}
|
||||
>
|
||||
{savingProfile ? 'Speichern…' : 'Name speichern'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginBottom: '1rem' }}>
|
||||
<h2 style={{ margin: '0 0 0.75rem', fontSize: '1.1rem' }}>Rollen & Tarif</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '0.5rem 1rem', fontSize: '0.925rem' }}>
|
||||
<strong style={{ color: 'var(--text2)' }}>Rolle</strong>
|
||||
<span>{user?.role === 'admin' ? 'Administrator' : user?.role || 'trainer'}</span>
|
||||
|
||||
<strong style={{ color: 'var(--text2)' }}>Tier</strong>
|
||||
<span style={{ textTransform: 'uppercase', letterSpacing: '0.03em', fontWeight: 600 }}>
|
||||
{user?.tier || 'free'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2 style={{ margin: '0 0 0.75rem', fontSize: '1.1rem' }}>Passwort ändern</h2>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem', lineHeight: 1.5 }}>
|
||||
Wähle ein neues Passwort (mindestens 4 Zeichen, wie beim Login gewohnt empfehlen wir längere Passwörter).
|
||||
</p>
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<div className="form-row" style={{ marginBottom: '0.75rem' }}>
|
||||
<label className="form-label" htmlFor="settings-pw1">
|
||||
Neues Passwort
|
||||
</label>
|
||||
<input
|
||||
id="settings-pw1"
|
||||
type="password"
|
||||
className="form-input"
|
||||
value={newPw1}
|
||||
onChange={(e) => setNewPw1(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
minLength={4}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: '0.75rem' }}>
|
||||
<label className="form-label" htmlFor="settings-pw2">
|
||||
Passwort wiederholen
|
||||
</label>
|
||||
<input
|
||||
id="settings-pw2"
|
||||
type="password"
|
||||
className="form-input"
|
||||
value={newPw2}
|
||||
onChange={(e) => setNewPw2(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-secondary" disabled={savingPw}>
|
||||
{savingPw ? 'Wird gespeichert…' : 'Passwort speichern'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountSettingsPage
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
function ProfilePage() {
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<div style={{ padding: '2rem' }}>
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h1>Profil</h1>
|
||||
|
||||
<div className="card" style={{ marginTop: '1.5rem' }}>
|
||||
<h2>Persönliche Daten</h2>
|
||||
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '150px 1fr', gap: '1rem', marginBottom: '1rem' }}>
|
||||
<strong>Name:</strong>
|
||||
<span>{user?.name || '-'}</span>
|
||||
|
||||
<strong>E-Mail:</strong>
|
||||
<span>{user?.email}</span>
|
||||
|
||||
<strong>Rolle:</strong>
|
||||
<span style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
background: user?.role === 'admin' ? 'var(--accent)' : 'var(--surface2)',
|
||||
color: user?.role === 'admin' ? 'white' : 'var(--text1)',
|
||||
borderRadius: '4px',
|
||||
display: 'inline-block',
|
||||
fontSize: '0.875rem'
|
||||
}}>
|
||||
{user?.role || 'user'}
|
||||
</span>
|
||||
|
||||
<strong>Tier:</strong>
|
||||
<span style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
background: user?.tier === 'premium' ? 'var(--accent)' : 'var(--surface2)',
|
||||
color: user?.tier === 'premium' ? 'white' : 'var(--text1)',
|
||||
borderRadius: '4px',
|
||||
display: 'inline-block',
|
||||
fontSize: '0.875rem'
|
||||
}}>
|
||||
{user?.tier || 'free'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: '1.5rem' }}>
|
||||
<h2>Einstellungen</h2>
|
||||
<p style={{ color: 'var(--text2)', marginTop: '0.5rem' }}>
|
||||
Bearbeitung folgt in Kürze
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfilePage
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user