From 45cadd07ce67e2c7accbd30936741c8e57f2f0ba Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 09:46:52 +0000 Subject: [PATCH] feat: add AccountPage with Profile, Security, and MFA tabs Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/AccountPage.tsx | 455 +++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 frontend/src/pages/AccountPage.tsx diff --git a/frontend/src/pages/AccountPage.tsx b/frontend/src/pages/AccountPage.tsx new file mode 100644 index 0000000..9896e4e --- /dev/null +++ b/frontend/src/pages/AccountPage.tsx @@ -0,0 +1,455 @@ +import { useState, useRef } from 'react' +import { useAuth } from '@/context/AuthContext' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Separator } from '@/components/ui/separator' +import { UserCog, Shield, Smartphone, Upload, Trash2, Check, Loader2 } from 'lucide-react' +import * as authService from '@/services/authService' +import type { ProfileUpdatePayload } from '@/types' + + +export function AccountPage() { + return ( +
+
+

Kontoverwaltung

+

+ Verwalten Sie Ihr Profil, Ihre Sicherheitseinstellungen und die Zwei-Faktor-Authentifizierung. +

+
+ + + + + + Profil + + + + Sicherheit + + + + Zwei-Faktor + + + + + + + + + + + + + +
+ ) +} + + +function ProfileTab() { + const { user, refreshUser } = useAuth() + const [saving, setSaving] = useState(false) + const [success, setSuccess] = useState('') + const [error, setError] = useState('') + const fileInputRef = useRef(null) + const [avatarUploading, setAvatarUploading] = useState(false) + + const [form, setForm] = useState({ + first_name: user?.first_name ?? '', + last_name: user?.last_name ?? '', + display_name: user?.display_name ?? '', + username: user?.username ?? '', + email: user?.email ?? '', + }) + + const handleSave = async () => { + setSaving(true) + setError('') + setSuccess('') + try { + await authService.updateProfile(form) + await refreshUser() + setSuccess('Profil erfolgreich gespeichert.') + } catch (err: any) { + setError(err.response?.data?.detail ?? 'Fehler beim Speichern.') + } finally { + setSaving(false) + } + } + + const handleAvatarUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + setAvatarUploading(true) + setError('') + try { + await authService.uploadAvatar(file) + await refreshUser() + } catch (err: any) { + setError(err.response?.data?.detail ?? 'Fehler beim Hochladen.') + } finally { + setAvatarUploading(false) + } + } + + const handleAvatarDelete = async () => { + setAvatarUploading(true) + try { + await authService.deleteAvatar() + await refreshUser() + } catch (err: any) { + setError(err.response?.data?.detail ?? 'Fehler beim Löschen.') + } finally { + setAvatarUploading(false) + } + } + + const initials = user?.username + ?.split(/[\s._-]/) + .map((p) => p[0]) + .join('') + .toUpperCase() + .slice(0, 2) ?? '??' + + const avatarSrc = user?.avatar_url ?? undefined + + return ( + + + Profildaten + Aktualisieren Sie Ihre persönlichen Informationen. + + + {error && {error}} + {success && {success}} + + {/* Avatar */} +
+ + {avatarSrc && } + {initials} + +
+ + + {user?.avatar_url && ( + + )} +
+
+ + + + {/* Form fields */} +
+
+ + setForm({ ...form, first_name: e.target.value })} /> +
+
+ + setForm({ ...form, last_name: e.target.value })} /> +
+
+
+ + setForm({ ...form, display_name: e.target.value })} placeholder="Wird automatisch aus Vor-/Nachname generiert, wenn leer" /> +
+
+
+ + setForm({ ...form, username: e.target.value })} /> +
+
+ + setForm({ ...form, email: e.target.value })} /> +
+
+ + +
+
+ ) +} + + +function SecurityTab() { + const { user } = useAuth() + const [saving, setSaving] = useState(false) + const [success, setSuccess] = useState('') + const [error, setError] = useState('') + const [form, setForm] = useState({ + old_password: '', + new_password: '', + confirm_password: '', + }) + + const handleChangePassword = async () => { + setError('') + setSuccess('') + if (form.new_password !== form.confirm_password) { + setError('Die Passwörter stimmen nicht überein.') + return + } + if (form.new_password.length < 8) { + setError('Das neue Passwort muss mindestens 8 Zeichen lang sein.') + return + } + setSaving(true) + try { + await authService.changePassword({ + old_password: form.old_password, + new_password: form.new_password, + }) + setSuccess('Passwort erfolgreich geändert.') + setForm({ old_password: '', new_password: '', confirm_password: '' }) + } catch (err: any) { + setError(err.response?.data?.detail ?? 'Fehler beim Ändern des Passworts.') + } finally { + setSaving(false) + } + } + + return ( +
+ + + Passwort ändern + Verwenden Sie ein starkes Passwort mit mindestens 8 Zeichen. + + + {error && {error}} + {success && {success}} +
+ + setForm({ ...form, old_password: e.target.value })} /> +
+
+ + setForm({ ...form, new_password: e.target.value })} /> +
+
+ + setForm({ ...form, confirm_password: e.target.value })} /> +
+ +
+
+ + + + Sitzungsinformationen + + +

+ Letzter Login:{' '} + {user?.last_login + ? new Date(user.last_login).toLocaleString('de-DE') + : 'Keine Daten verfügbar'} +

+
+
+
+ ) +} + + +function MFATab() { + const { user, refreshUser } = useAuth() + const [step, setStep] = useState<'idle' | 'setup' | 'verify'>('idle') + const [secret, setSecret] = useState('') + const [qrUri, setQrUri] = useState('') + const [code, setCode] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [showDisableDialog, setShowDisableDialog] = useState(false) + + const handleStartSetup = async () => { + setLoading(true) + setError('') + try { + const result = await authService.setupMFA() + setSecret(result.secret) + setQrUri(result.qr_uri) + setStep('verify') + } catch (err: any) { + setError(err.response?.data?.detail ?? 'Fehler beim MFA-Setup.') + } finally { + setLoading(false) + } + } + + const handleVerify = async () => { + setLoading(true) + setError('') + try { + await authService.verifyMFA({ secret, code }) + await refreshUser() + setSuccess('Zwei-Faktor-Authentifizierung erfolgreich aktiviert!') + setStep('idle') + setCode('') + } catch (err: any) { + setError(err.response?.data?.detail ?? 'Ungültiger Code. Bitte erneut versuchen.') + } finally { + setLoading(false) + } + } + + const handleDisable = async () => { + setLoading(true) + setError('') + try { + await authService.disableMFA(password) + await refreshUser() + setSuccess('Zwei-Faktor-Authentifizierung deaktiviert.') + setShowDisableDialog(false) + setPassword('') + } catch (err: any) { + setError(err.response?.data?.detail ?? 'Fehler beim Deaktivieren.') + } finally { + setLoading(false) + } + } + + return ( + + + + Zwei-Faktor-Authentifizierung + {user?.mfa_enabled && ( + + Aktiv + + )} + + + Schützen Sie Ihr Konto mit einem zusätzlichen Sicherheitsfaktor über eine Authenticator-App. + + + + {error && {error}} + {success && {success}} + + {!user?.mfa_enabled && step === 'idle' && ( +
+

+ Die Zwei-Faktor-Authentifizierung ist derzeit nicht aktiviert. Aktivieren Sie diese Funktion, + um Ihr Konto zusätzlich zu schützen. +

+ +
+ )} + + {step === 'verify' && ( +
+

+ Scannen Sie den folgenden QR-Code mit Ihrer Authenticator-App (z.B. Google Authenticator, Authy): +

+
+ QR Code für 2FA +
+

+ Manueller Schlüssel: {secret} +

+
+ + setCode(e.target.value)} + placeholder="6-stelliger Code" + maxLength={6} + /> +
+
+ + +
+
+ )} + + {user?.mfa_enabled && step === 'idle' && !showDisableDialog && ( +
+

+ Ihr Konto ist durch Zwei-Faktor-Authentifizierung geschützt. +

+ +
+ )} + + {showDisableDialog && ( +
+

+ Bitte bestätigen Sie mit Ihrem Passwort, um 2FA zu deaktivieren. +

+
+ + setPassword(e.target.value)} + /> +
+
+ + +
+
+ )} +
+
+ ) +}