// frontend\src\pages\settings\konto\Konto.tsx import { useEffect, useState, type FormEvent, type ReactNode, } from 'react' import Button from '../../../components/Button' import Container from '../../../components/Container' import type { User } from '../../../components/types' import { ComputerDesktopIcon, ShieldCheckIcon, } from '@heroicons/react/20/solid' import Notifications, { type ToastNotification, } from '../../../components/Notifications' const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8080' type KontoProps = { user?: User onUserUpdated?: (user: User) => void } type UserSession = { id: string browser: string operatingSystem: string deviceType: string userAgent: string ipAddress: string createdAt: string lastSeenAt: string current: boolean } const inputClassName = 'block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500' function Section({ title, description, children, }: { title: string description: string children: ReactNode }) { return (

{title}

{description}

{children}
) } async function readApiError(response: Response, fallback: string) { const data = await response.json().catch(() => null) return data?.error ?? fallback } export default function Konto({ user, onUserUpdated, }: KontoProps) { const [toasts, setToasts] = useState([]) const [currentUser, setCurrentUser] = useState(user) const [sessions, setSessions] = useState([]) const [isLoadingSessions, setIsLoadingSessions] = useState(false) const [revokingSessionId, setRevokingSessionId] = useState(null) const [isSavingKonto, setIsSavingKonto] = useState(false) const [isSavingPassword, setIsSavingPassword] = useState(false) const [isLoggingOutSessions, setIsLoggingOutSessions] = useState(false) const [isDeletingKonto, setIsDeletingKonto] = useState(false) useEffect(() => { setCurrentUser(user) }, [user]) useEffect(() => { void loadSessions() }, []) const avatarUrl = currentUser?.avatar || 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80' function dismissToast(id: string) { setToasts((currentToasts) => currentToasts.filter((toast) => toast.id !== id), ) } function showToast(notification: Omit) { setToasts((currentToasts) => [ ...currentToasts, { id: crypto.randomUUID(), ...notification, }, ]) } async function loadSessions() { setIsLoadingSessions(true) try { const response = await fetch(`${API_URL}/settings/sessions`, { credentials: 'include', }) if (!response.ok) { throw new Error( await readApiError(response, 'Sitzungen konnten nicht geladen werden'), ) } const data = await response.json() setSessions(data.sessions ?? []) } catch (error) { showToast({ title: 'Sitzungen konnten nicht geladen werden', message: error instanceof Error ? error.message : 'Sitzungen konnten nicht geladen werden', variant: 'error', }) } finally { setIsLoadingSessions(false) } } async function handleRevokeSession(sessionId: string) { setRevokingSessionId(sessionId) try { const response = await fetch(`${API_URL}/settings/sessions/${sessionId}`, { method: 'DELETE', credentials: 'include', }) if (!response.ok) { throw new Error( await readApiError(response, 'Sitzung konnte nicht abgemeldet werden'), ) } await loadSessions() showToast({ title: 'Sitzung abgemeldet', message: 'Die ausgewählte Sitzung wurde abgemeldet.', variant: 'success', }) } catch (error) { showToast({ title: 'Sitzung konnte nicht abgemeldet werden', message: error instanceof Error ? error.message : 'Sitzung konnte nicht abgemeldet werden', variant: 'error', autoClose: false, }) } finally { setRevokingSessionId(null) } } async function handleKontoSubmit(event: FormEvent) { event.preventDefault() const form = event.currentTarget const formData = new FormData(form) setIsSavingKonto(true) try { const response = await fetch(`${API_URL}/settings/account`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ displayName: String(formData.get('displayName') ?? ''), username: String(formData.get('username') ?? ''), email: String(formData.get('email') ?? ''), avatar: String(formData.get('avatar') ?? ''), unit: String(formData.get('unit') ?? ''), group: String(formData.get('group') ?? ''), }), }) if (!response.ok) { throw new Error( await readApiError(response, 'Profil konnte nicht gespeichert werden'), ) } const data = await response.json().catch(() => null) const nextUser = data?.user as User | undefined if (nextUser) { setCurrentUser(nextUser) onUserUpdated?.(nextUser) } showToast({ title: 'Profil gespeichert', message: 'Deine Profildaten wurden gespeichert.', variant: 'success', }) } catch (error) { showToast({ title: 'Profil konnte nicht gespeichert werden', message: error instanceof Error ? error.message : 'Profil konnte nicht gespeichert werden', variant: 'error', autoClose: false, }) } finally { setIsSavingKonto(false) } } async function handlePasswordSubmit(event: FormEvent) { event.preventDefault() const form = event.currentTarget const formData = new FormData(form) setIsSavingPassword(true) try { const response = await fetch(`${API_URL}/settings/password`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ currentPassword: String(formData.get('currentPassword') ?? ''), newPassword: String(formData.get('newPassword') ?? ''), confirmPassword: String(formData.get('confirmPassword') ?? ''), }), }) if (!response.ok) { throw new Error( await readApiError(response, 'Passwort konnte nicht gespeichert werden'), ) } form.reset() showToast({ title: 'Passwort gespeichert', message: 'Dein Passwort wurde erfolgreich geändert.', variant: 'success', }) } catch (error) { showToast({ title: 'Passwort konnte nicht gespeichert werden', message: error instanceof Error ? error.message : 'Passwort konnte nicht gespeichert werden', variant: 'error', autoClose: false, }) } finally { setIsSavingPassword(false) } } async function handleLogoutSessionsSubmit(event: FormEvent) { event.preventDefault() const form = event.currentTarget const formData = new FormData(form) setIsLoggingOutSessions(true) try { const response = await fetch(`${API_URL}/settings/logout-other-sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ password: String(formData.get('password') ?? ''), }), }) if (!response.ok) { throw new Error( await readApiError( response, 'Andere Sitzungen konnten nicht abgemeldet werden', ), ) } form.reset() showToast({ title: 'Andere Sitzungen abgemeldet', message: 'Alle anderen aktiven Sitzungen wurden abgemeldet.', variant: 'success', }) await loadSessions() } catch (error) { showToast({ title: 'Andere Sitzungen konnten nicht abgemeldet werden', message: error instanceof Error ? error.message : 'Andere Sitzungen konnten nicht abgemeldet werden', variant: 'error', autoClose: false, }) } finally { setIsLoggingOutSessions(false) } } async function handleDeleteKontoSubmit(event: FormEvent) { event.preventDefault() const form = event.currentTarget const formData = new FormData(form) const confirmed = window.confirm( 'Konto wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.', ) if (!confirmed) { return } setIsDeletingKonto(true) try { const response = await fetch(`${API_URL}/settings/account`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ password: String(formData.get('password') ?? ''), }), }) if (!response.ok) { throw new Error( await readApiError(response, 'Konto konnte nicht gelöscht werden'), ) } window.location.href = '/login' } catch (error) { showToast({ title: 'Konto konnte nicht gelöscht werden', message: error instanceof Error ? error.message : 'Konto konnte nicht gelöscht werden', variant: 'error', autoClose: false, }) } finally { setIsDeletingKonto(false) } } return ( <>

Gib eine Bild-URL für dein Profilbild an.

{isLoadingSessions && (
Sitzungen werden geladen...
)} {!isLoadingSessions && sessions.length > 0 && (
{sessions.map((session) => (

{session.current ? 'Aktuelle Sitzung' : 'Weitere Sitzung'}

{session.browser} auf {session.operatingSystem}

{session.deviceType} {session.ipAddress ? ` · ${session.ipAddress}` : ''}

Zuletzt aktiv:{' '} {new Intl.DateTimeFormat('de-DE', { dateStyle: 'short', timeStyle: 'short', }).format(new Date(session.lastSeenAt))}

{session.current ? ( ) : ( )}
))}
)} {!isLoadingSessions && sessions.length === 0 && (
Keine aktiven Sitzungen gefunden.
)}

Dadurch werden alle anderen aktiven Sitzungen abgemeldet. Diese Sitzung bleibt aktiv.

) }