831 lines
25 KiB
TypeScript
831 lines
25 KiB
TypeScript
// frontend\src\pages\settings\konto\Konto.tsx
|
|
|
|
import {
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
type ChangeEvent,
|
|
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 { useNotifications } from '../../../components/Notifications'
|
|
import Avatar from '../../../components/Avatar'
|
|
import Combobox, { type ComboboxItem } from '../../../components/Combobox'
|
|
|
|
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 (
|
|
<Container variant="constrained" className="py-16">
|
|
<div className="grid grid-cols-1 gap-x-8 gap-y-10 md:grid-cols-3">
|
|
<div>
|
|
<h2 className="text-base/7 font-semibold text-gray-900 dark:text-white">
|
|
{title}
|
|
</h2>
|
|
|
|
<p className="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
|
|
{description}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
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 { showToast, showErrorToast } = useNotifications()
|
|
|
|
const [currentUser, setCurrentUser] = useState<User | undefined>(user)
|
|
const [avatarPreview, setAvatarPreview] = useState(user?.avatar ?? '')
|
|
const avatarInputRef = useRef<HTMLInputElement | null>(null)
|
|
const [sessions, setSessions] = useState<UserSession[]>([])
|
|
|
|
const [isLoadingSessions, setIsLoadingSessions] = useState(false)
|
|
const [revokingSessionId, setRevokingSessionId] = useState<string | null>(null)
|
|
const [isSavingKonto, setIsSavingKonto] = useState(false)
|
|
const [isSavingPassword, setIsSavingPassword] = useState(false)
|
|
const [isLoggingOutSessions, setIsLoggingOutSessions] = useState(false)
|
|
const [isDeletingKonto, setIsDeletingKonto] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setCurrentUser(user)
|
|
setAvatarPreview(user?.avatar ?? '')
|
|
}, [user])
|
|
|
|
useEffect(() => {
|
|
void loadSessions()
|
|
}, [])
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
function handleAvatarFileChange(event: ChangeEvent<HTMLInputElement>) {
|
|
const file = event.target.files?.[0]
|
|
|
|
if (!file) {
|
|
return
|
|
}
|
|
|
|
if (!file.type.startsWith('image/')) {
|
|
showToast({
|
|
title: 'Ungültige Datei',
|
|
message: 'Bitte wähle eine gültige Bilddatei aus.',
|
|
variant: 'error',
|
|
})
|
|
event.target.value = ''
|
|
return
|
|
}
|
|
|
|
if (file.size > 2 * 1024 * 1024) {
|
|
showToast({
|
|
title: 'Bild zu groß',
|
|
message: 'Das Profilbild darf maximal 2 MB groß sein.',
|
|
variant: 'error',
|
|
})
|
|
event.target.value = ''
|
|
return
|
|
}
|
|
|
|
const reader = new FileReader()
|
|
|
|
reader.onload = () => {
|
|
if (typeof reader.result === 'string') {
|
|
setAvatarPreview(reader.result)
|
|
}
|
|
}
|
|
|
|
reader.readAsDataURL(file)
|
|
event.target.value = ''
|
|
}
|
|
|
|
function handleRemoveAvatar() {
|
|
setAvatarPreview('')
|
|
}
|
|
|
|
async function handleKontoSubmit(event: FormEvent<HTMLFormElement>) {
|
|
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') ?? ''),
|
|
}),
|
|
})
|
|
|
|
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)
|
|
setAvatarPreview(nextUser.avatar ?? '')
|
|
onUserUpdated?.(nextUser)
|
|
}
|
|
|
|
showToast({
|
|
title: 'Profil gespeichert',
|
|
message: 'Deine Profildaten wurden gespeichert.',
|
|
variant: 'success',
|
|
})
|
|
} catch (error) {
|
|
showErrorToast({
|
|
title: 'Profil konnte nicht gespeichert werden',
|
|
error,
|
|
fallback: 'Profil konnte nicht gespeichert werden',
|
|
})
|
|
} finally {
|
|
setIsSavingKonto(false)
|
|
}
|
|
}
|
|
|
|
async function handlePasswordSubmit(event: FormEvent<HTMLFormElement>) {
|
|
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<HTMLFormElement>) {
|
|
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<HTMLFormElement>) {
|
|
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 (
|
|
<>
|
|
<div className="divide-y divide-gray-200 dark:divide-white/10">
|
|
<Section
|
|
title="Persönliche Informationen"
|
|
description="Verwalte deine Profildaten und Kontoinformationen."
|
|
>
|
|
<form onSubmit={handleKontoSubmit}>
|
|
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
|
|
<div className="col-span-full flex items-center gap-x-8">
|
|
<Avatar
|
|
src={avatarPreview}
|
|
name={currentUser?.displayName || currentUser?.username || currentUser?.email}
|
|
size={16}
|
|
shape="rounded"
|
|
/>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<input
|
|
type="hidden"
|
|
name="avatar"
|
|
value={avatarPreview}
|
|
/>
|
|
|
|
<input
|
|
ref={avatarInputRef}
|
|
type="file"
|
|
accept="image/png,image/jpeg,image/webp,image/gif"
|
|
className="sr-only"
|
|
onChange={handleAvatarFileChange}
|
|
/>
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
color="gray"
|
|
size="sm"
|
|
onClick={() => avatarInputRef.current?.click()}
|
|
>
|
|
Bild hochladen
|
|
</Button>
|
|
|
|
{avatarPreview && (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
color="red"
|
|
size="sm"
|
|
onClick={handleRemoveAvatar}
|
|
>
|
|
Entfernen
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<p className="mt-2 text-xs/5 text-gray-500 dark:text-gray-400">
|
|
PNG, JPG, WEBP oder GIF bis maximal 2 MB.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="sm:col-span-3">
|
|
<label
|
|
htmlFor="displayName"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Anzeigename
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="displayName"
|
|
name="displayName"
|
|
type="text"
|
|
defaultValue={currentUser?.displayName ?? ''}
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="sm:col-span-3">
|
|
<label
|
|
htmlFor="username"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Benutzername
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="username"
|
|
name="username"
|
|
type="text"
|
|
required
|
|
defaultValue={currentUser?.username ?? ''}
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-span-full">
|
|
<label
|
|
htmlFor="email"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
E-Mail-Adresse
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
defaultValue={currentUser?.email ?? ''}
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-span-full">
|
|
<Combobox
|
|
label="Einheit"
|
|
items={
|
|
currentUser?.unit
|
|
? [{ id: currentUser.unit, name: currentUser.unit }]
|
|
: []
|
|
}
|
|
value={
|
|
currentUser?.unit
|
|
? ({
|
|
id: currentUser.unit,
|
|
name: currentUser.unit,
|
|
} satisfies ComboboxItem)
|
|
: null
|
|
}
|
|
onChange={() => undefined}
|
|
placeholder="Keine Einheit zugeordnet"
|
|
disabled
|
|
/>
|
|
<p className="mt-2 text-xs/5 text-gray-500 dark:text-gray-400">
|
|
Die Einheit kann nur in der Benutzeradministration geändert werden.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 flex">
|
|
<Button
|
|
type="submit"
|
|
color="indigo"
|
|
size="md"
|
|
isLoading={isSavingKonto}
|
|
>
|
|
Speichern
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Section>
|
|
|
|
<Section
|
|
title="Passwort ändern"
|
|
description="Aktualisiere das Passwort deines Benutzerkontos."
|
|
>
|
|
<form onSubmit={handlePasswordSubmit}>
|
|
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
|
|
<div className="col-span-full">
|
|
<label
|
|
htmlFor="current-password"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Aktuelles Passwort
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="current-password"
|
|
name="currentPassword"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
required
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-span-full">
|
|
<label
|
|
htmlFor="new-password"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Neues Passwort
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="new-password"
|
|
name="newPassword"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
minLength={8}
|
|
required
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="col-span-full">
|
|
<label
|
|
htmlFor="confirm-password"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Neues Passwort bestätigen
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="confirm-password"
|
|
name="confirmPassword"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
minLength={8}
|
|
required
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 flex">
|
|
<Button
|
|
type="submit"
|
|
color="indigo"
|
|
size="md"
|
|
isLoading={isSavingPassword}
|
|
>
|
|
Passwort speichern
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Section>
|
|
|
|
<Section
|
|
title="Sitzungen"
|
|
description="Sieh dir deine aktiven Sitzungen an und melde andere Browser ab."
|
|
>
|
|
<div className="sm:max-w-2xl">
|
|
{isLoadingSessions && (
|
|
<div className="rounded-md border border-dashed border-gray-300 p-4 text-sm text-gray-500 dark:border-white/10 dark:text-gray-400">
|
|
Sitzungen werden geladen...
|
|
</div>
|
|
)}
|
|
|
|
{!isLoadingSessions && sessions.length > 0 && (
|
|
<div className="space-y-3">
|
|
{sessions.map((session) => (
|
|
<div
|
|
key={session.id}
|
|
className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5"
|
|
>
|
|
<div className="flex items-start gap-x-4">
|
|
<div className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-indigo-50 text-indigo-600 ring-1 ring-indigo-600/10 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20">
|
|
<ComputerDesktopIcon
|
|
aria-hidden="true"
|
|
className="size-6"
|
|
/>
|
|
</div>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-start justify-between gap-x-4">
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{session.current ? 'Aktuelle Sitzung' : 'Weitere Sitzung'}
|
|
</h3>
|
|
|
|
<p className="mt-1 text-sm text-gray-700 dark:text-gray-300">
|
|
{session.browser} auf {session.operatingSystem}
|
|
</p>
|
|
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{session.deviceType}
|
|
{session.ipAddress ? ` · ${session.ipAddress}` : ''}
|
|
</p>
|
|
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Zuletzt aktiv:{' '}
|
|
{new Intl.DateTimeFormat('de-DE', {
|
|
dateStyle: 'short',
|
|
timeStyle: 'short',
|
|
}).format(new Date(session.lastSeenAt))}
|
|
</p>
|
|
</div>
|
|
|
|
{session.current ? (
|
|
<span className="inline-flex shrink-0 items-center gap-x-1.5 rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset dark:bg-green-900/30 dark:text-green-300 dark:ring-green-500/30">
|
|
<ShieldCheckIcon
|
|
aria-hidden="true"
|
|
className="size-3.5"
|
|
/>
|
|
Aktiv
|
|
</span>
|
|
) : (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
color="red"
|
|
size="sm"
|
|
isLoading={revokingSessionId === session.id}
|
|
onClick={() => void handleRevokeSession(session.id)}
|
|
>
|
|
Abmelden
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{!isLoadingSessions && sessions.length === 0 && (
|
|
<div className="rounded-md border border-dashed border-gray-300 p-4 text-sm text-gray-500 dark:border-white/10 dark:text-gray-400">
|
|
Keine aktiven Sitzungen gefunden.
|
|
</div>
|
|
)}
|
|
|
|
<form
|
|
onSubmit={handleLogoutSessionsSubmit}
|
|
className="mt-8 border-t border-gray-200 pt-6 dark:border-white/10"
|
|
>
|
|
<label
|
|
htmlFor="logout-password"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Dein Passwort
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="logout-password"
|
|
name="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
required
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
|
|
<p className="mt-2 text-xs/5 text-gray-500 dark:text-gray-400">
|
|
Dadurch werden alle anderen aktiven Sitzungen abgemeldet. Diese Sitzung bleibt aktiv.
|
|
</p>
|
|
|
|
<div className="mt-6 flex">
|
|
<Button
|
|
type="submit"
|
|
color="indigo"
|
|
size="md"
|
|
isLoading={isLoggingOutSessions}
|
|
>
|
|
Andere Sitzungen abmelden
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section
|
|
title="Konto löschen"
|
|
description="Das Löschen deines Kontos kann nicht rückgängig gemacht werden."
|
|
>
|
|
<form onSubmit={handleDeleteKontoSubmit}>
|
|
<div className="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
|
|
<div className="col-span-full">
|
|
<label
|
|
htmlFor="delete-konto-password"
|
|
className="block text-sm/6 font-medium text-gray-900 dark:text-white"
|
|
>
|
|
Dein Passwort
|
|
</label>
|
|
|
|
<div className="mt-2">
|
|
<input
|
|
id="delete-konto-password"
|
|
name="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
required
|
|
className={inputClassName}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 flex">
|
|
<Button
|
|
type="submit"
|
|
color="red"
|
|
size="md"
|
|
isLoading={isDeletingKonto}
|
|
>
|
|
Konto löschen
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Section>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|