feat: Stammdaten, Ersteinrichtung, Zweitfaktor und Protokollansicht
Damit ist Etappe 1 fachlich vollstaendig. Ersteinrichtung in drei Schritten, bewusst ausserhalb des Anwendungsrahmens: der setzt Benutzer, zweiten Faktor UND eine betreute Person voraus - beim allerersten Start gibt es nichts davon. Die Aktion fuer den ersten Zugang sperrt sich selbst, sobald ein Benutzer existiert. Zweiter Faktor: Das Geheimnis wird zuerst nur abgelegt, ohne totpAktiviertAm. Erst ein echter Code aus der App schaltet ihn scharf. Wer sich beim Abtippen vertut oder die falsche App nimmt, wuerde sich sonst dauerhaft aussperren. Erst danach gibt es die Wiederherstellungscodes - einmal angezeigt, danach nur noch als Hash. Betreuungsjahr (src/lib/stammdaten/betreuungsjahr.ts): laeuft ab dem Tag der Bestellung, nicht ab dem Kalenderjahr. Schrittweise vorwaerts gerechnet statt ueber eine Differenz, damit Schaltjahre und der 29. Februar von selbst stimmen. Daran haengen Jahresbericht, Rechnungslegung und die Ausschlussfrist der Aufwandspauschale. Aendert sich das Bestellungsdatum, werden die Pflichtfristen neu abgeleitet - ueber einen dynamischen Import, damit die Stammdaten auch dann speicherbar bleiben, wenn das Fristenmodul fehlt. Protokollansicht zeigt je Eintrag die geaenderten Felder als alt -> neu. Die Aenderungen liegen verschluesselt, weil dort Diagnosen und Kontostaende stehen koennen. Seed legt bewusst keine echten Personendaten an, nur den ersten Zugang. Mit SEED_DEMO=true kommt ein vollstaendiger Beispielfall dazu - Erika Musterfrau, COPD GOLD III, Pflegegrad 3, Heimeinzug Januar 2026, leerstehendes Haus, zwei Versicherungen, laufender Antrag beim Bezirk mit acht Nachforderungen. Nur ausserhalb der Produktion. Docker Compose ohne Reverse Proxy - der ist auf dem Zielserver vorhanden. Die Datenbank hat keine Portfreigabe und ist nur ueber das interne Netz erreichbar; die Anwendung horcht auf 127.0.0.1. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { useFormStatus } from 'react-dom'
|
||||
import { AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { betreuteAnlegen, type BetreuteZustand } from '@/server/actions/betreute'
|
||||
import { AUFGABENKREISE, AUFGABENKREIS_BESCHRIFTUNG } from '@/lib/validierung/betreute'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Feld } from '@/components/feld'
|
||||
import { Rechtshinweis } from '@/components/rechtshinweis'
|
||||
|
||||
function Absenden() {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" className="w-full" size="lg" disabled={pending}>
|
||||
{pending && <Loader2 className="animate-spin" aria-hidden="true" />}
|
||||
{pending ? 'Wird angelegt …' : 'Betreuung anlegen und loslegen'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/** Nur die vier häufigsten Aufgabenkreise werden vorangestellt. */
|
||||
const HAEUFIG = ['VERMOEGENSSORGE', 'GESUNDHEITSSORGE', 'AUFENTHALTSBESTIMMUNG', 'WOHNUNGSANGELEGENHEITEN']
|
||||
|
||||
export function BetreuungFormular() {
|
||||
const [zustand, aktion] = useActionState<BetreuteZustand, FormData>(betreuteAnlegen, {})
|
||||
|
||||
return (
|
||||
<form action={aktion} className="space-y-5" noValidate>
|
||||
{zustand.fehler && (
|
||||
<Alert variant="fehler">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertDescription>{zustand.fehler}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Feld id="vorname" beschriftung="Vorname" fehler={zustand.felder?.vorname} pflicht>
|
||||
<Input name="vorname" defaultValue={zustand.werte?.vorname} required autoFocus />
|
||||
</Feld>
|
||||
<Feld id="nachname" beschriftung="Nachname" fehler={zustand.felder?.nachname} pflicht>
|
||||
<Input name="nachname" defaultValue={zustand.werte?.nachname} required />
|
||||
</Feld>
|
||||
</div>
|
||||
|
||||
<Feld
|
||||
id="geburtsdatum"
|
||||
beschriftung="Geburtsdatum"
|
||||
fehler={zustand.felder?.geburtsdatum}
|
||||
pflicht
|
||||
>
|
||||
<Input
|
||||
name="geburtsdatum"
|
||||
type="date"
|
||||
defaultValue={zustand.werte?.geburtsdatum}
|
||||
required
|
||||
/>
|
||||
</Feld>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Feld
|
||||
id="betreuungsgericht"
|
||||
beschriftung="Betreuungsgericht"
|
||||
fehler={zustand.felder?.betreuungsgericht}
|
||||
>
|
||||
<Input
|
||||
name="betreuungsgericht"
|
||||
placeholder="Amtsgericht …"
|
||||
defaultValue={zustand.werte?.betreuungsgericht}
|
||||
/>
|
||||
</Feld>
|
||||
<Feld
|
||||
id="aktenzeichenGericht"
|
||||
beschriftung="Aktenzeichen"
|
||||
fehler={zustand.felder?.aktenzeichenGericht}
|
||||
>
|
||||
<Input
|
||||
name="aktenzeichenGericht"
|
||||
placeholder="XVII 123/25"
|
||||
defaultValue={zustand.werte?.aktenzeichenGericht}
|
||||
/>
|
||||
</Feld>
|
||||
</div>
|
||||
|
||||
<Feld
|
||||
id="bestellungAm"
|
||||
beschriftung="Datum der Bestellung zum Betreuer"
|
||||
erklaerung="Steht in deiner Bestellungsurkunde. Daraus ergibt sich das Betreuungsjahr — und damit, wann Anfangsbericht, Jahresbericht und die Aufwandspauschale fällig sind."
|
||||
fehler={zustand.felder?.bestellungAm}
|
||||
>
|
||||
<Input name="bestellungAm" type="date" defaultValue={zustand.werte?.bestellungAm} />
|
||||
</Feld>
|
||||
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="text-sm font-medium">Aufgabenkreise</legend>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Die in der Bestellungsurkunde genannten Bereiche.
|
||||
</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{[...HAEUFIG, ...AUFGABENKREISE.filter((a) => !HAEUFIG.includes(a))].map((kreis) => (
|
||||
<label
|
||||
key={kreis}
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-md border p-2.5 hover:bg-accent"
|
||||
>
|
||||
<Checkbox
|
||||
name="aufgabenkreise"
|
||||
value={kreis}
|
||||
defaultChecked={
|
||||
kreis === 'VERMOEGENSSORGE' || kreis === 'GESUNDHEITSSORGE'
|
||||
}
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{AUFGABENKREIS_BESCHRIFTUNG[kreis as keyof typeof AUFGABENKREIS_BESCHRIFTUNG]}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label className="flex cursor-pointer items-start gap-2.5 rounded-md border p-3 hover:bg-accent">
|
||||
<Checkbox name="befreiterBetreuer" defaultChecked className="mt-0.5" />
|
||||
<span className="text-sm">
|
||||
<span className="font-medium">Befreiter Betreuer (§ 1859 BGB)</span>
|
||||
<span className="mt-0.5 block text-muted-foreground">
|
||||
Trifft auf Angehörige in der Regel zu. Statt der vollständigen Rechnungslegung genügt
|
||||
dann eine Vermögensübersicht.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-start gap-2.5 rounded-md border p-3 hover:bg-accent">
|
||||
<Checkbox name="ehrenamtlich" defaultChecked className="mt-0.5" />
|
||||
<span className="text-sm">
|
||||
<span className="font-medium">Ehrenamtliche Betreuung</span>
|
||||
<span className="mt-0.5 block text-muted-foreground">
|
||||
Dann steht dir die jährliche Aufwandspauschale nach § 1878 BGB zu — die Anwendung
|
||||
erinnert dich rechtzeitig, weil der Anspruch sonst verfällt.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Rechtshinweis rechtsgrundlage="§§ 1859, 1863, 1878 BGB" />
|
||||
|
||||
<Absenden />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { KeyRound, UserPlus } from 'lucide-react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { aktuellerBenutzer, meineBetreuten } from '@/lib/auth/sitzung'
|
||||
import { db } from '@/lib/db'
|
||||
import { ZugangFormular } from './zugang-formular'
|
||||
import { BetreuungFormular } from './betreuung-formular'
|
||||
|
||||
export const metadata: Metadata = { title: 'Ersteinrichtung' }
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Ersteinrichtung in drei Schritten.
|
||||
*
|
||||
* Diese Seite liegt bewusst ausserhalb des Anwendungsrahmens: der Rahmen
|
||||
* setzt einen Benutzer, einen zweiten Faktor UND eine betreute Person
|
||||
* voraus. Beim allerersten Start gibt es nichts davon.
|
||||
*/
|
||||
export default async function ErsteinrichtungSeite() {
|
||||
const anzahlBenutzer = await db.user.count({ where: { geloeschtAm: null } })
|
||||
|
||||
// --- Schritt 1: Es gibt noch keinen Zugang -------------------------------
|
||||
if (anzahlBenutzer === 0) {
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<UserPlus className="size-5" aria-hidden="true" />
|
||||
Schritt 1 von 3: Dein Zugang
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Dieser erste Zugang verwaltet auch die Anwendung. Danach richtest du den zweiten
|
||||
Faktor ein und legst die betreute Person an.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ZugangFormular />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Alert variant="hinweis">
|
||||
<KeyRound aria-hidden="true" />
|
||||
<AlertTitle>Zum Passwort</AlertTitle>
|
||||
<AlertDescription>
|
||||
Nimm vier zufällige Wörter statt eines kurzen Kunstworts —{' '}
|
||||
<em>Kastanie-Waschbär-Turm-Grün</em> ist leichter zu merken und deutlich schwerer zu
|
||||
knacken. Ein Passwortmanager ist noch besser.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const benutzer = await aktuellerBenutzer()
|
||||
if (!benutzer) redirect('/anmelden')
|
||||
|
||||
// --- Schritt 2: Zweiter Faktor fehlt noch --------------------------------
|
||||
if (!benutzer.totpAktiviertAm) redirect('/zwei-faktor/einrichten')
|
||||
|
||||
// --- Schritt 3: Noch keine betreute Person ------------------------------
|
||||
const betreute = await meineBetreuten(benutzer)
|
||||
if (betreute.length > 0) redirect('/heute')
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Schritt 3 von 3: Die betreute Person</CardTitle>
|
||||
<CardDescription>
|
||||
Nur das Nötigste — alles Weitere trägst du später in Ruhe unter Stammdaten nach. Das{' '}
|
||||
<strong>Bestellungsdatum</strong> ist das wichtigste Feld: daraus berechnet die Anwendung
|
||||
das Betreuungsjahr und damit deine gesetzlichen Fristen.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BetreuungFormular />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { useFormStatus } from 'react-dom'
|
||||
import { AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { erstenZugangAnlegen, type ZugangZustand } from '@/server/actions/ersteinrichtung'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Feld } from '@/components/feld'
|
||||
|
||||
function Absenden() {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" className="w-full" size="lg" disabled={pending}>
|
||||
{pending && <Loader2 className="animate-spin" aria-hidden="true" />}
|
||||
{pending ? 'Wird angelegt …' : 'Zugang anlegen'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ZugangFormular() {
|
||||
const [zustand, aktion] = useActionState<ZugangZustand, FormData>(erstenZugangAnlegen, {})
|
||||
|
||||
return (
|
||||
<form action={aktion} className="space-y-4" noValidate>
|
||||
{zustand.fehler && (
|
||||
<Alert variant="fehler">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertDescription>{zustand.fehler}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Feld id="name" beschriftung="Dein Name" fehler={zustand.felder?.name} pflicht>
|
||||
<Input
|
||||
name="name"
|
||||
autoComplete="name"
|
||||
defaultValue={zustand.werte?.name}
|
||||
placeholder="Vorname Nachname"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="email"
|
||||
beschriftung="E-Mail-Adresse"
|
||||
erklaerung="Damit meldest du dich an. Sie wird nicht nach außen versendet."
|
||||
fehler={zustand.felder?.email}
|
||||
pflicht
|
||||
>
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="username"
|
||||
inputMode="email"
|
||||
autoCapitalize="none"
|
||||
spellCheck={false}
|
||||
defaultValue={zustand.werte?.email}
|
||||
required
|
||||
/>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="passwort"
|
||||
beschriftung="Passwort"
|
||||
erklaerung="Mindestens 12 Zeichen."
|
||||
fehler={zustand.felder?.passwort}
|
||||
pflicht
|
||||
>
|
||||
<Input name="passwort" type="password" autoComplete="new-password" required />
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="passwortWiederholung"
|
||||
beschriftung="Passwort wiederholen"
|
||||
fehler={zustand.felder?.passwortWiederholung}
|
||||
pflicht
|
||||
>
|
||||
<Input name="passwortWiederholung" type="password" autoComplete="new-password" required />
|
||||
</Feld>
|
||||
|
||||
<Absenden />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
'use client'
|
||||
|
||||
import { useActionState, useState } from 'react'
|
||||
import { useFormStatus } from 'react-dom'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { AlertCircle, Check, Copy, Loader2, ShieldCheck } from 'lucide-react'
|
||||
import {
|
||||
zweitenFaktorBestaetigen,
|
||||
type BestaetigungsZustand,
|
||||
} from '@/server/actions/zwei-faktor'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Feld } from '@/components/feld'
|
||||
|
||||
function Absenden() {
|
||||
const { pending } = useFormStatus()
|
||||
return (
|
||||
<Button type="submit" className="w-full" size="lg" disabled={pending}>
|
||||
{pending && <Loader2 className="animate-spin" aria-hidden="true" />}
|
||||
{pending ? 'Wird geprüft …' : 'Code bestätigen und aktivieren'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/** Base32 in Vierergruppen — so tippt es sich von Hand deutlich sicherer. */
|
||||
function gruppiert(secret: string): string {
|
||||
return secret.replace(/(.{4})/g, '$1 ').trim()
|
||||
}
|
||||
|
||||
export function EinrichtungsFormular({ qrCode, secret }: { qrCode: string; secret: string }) {
|
||||
const [zustand, aktion] = useActionState<BestaetigungsZustand, FormData>(
|
||||
zweitenFaktorBestaetigen,
|
||||
{},
|
||||
)
|
||||
const [kopiert, setKopiert] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
// --- Nach erfolgreicher Aktivierung: die Codes, genau einmal ------------
|
||||
if (zustand.wiederherstellungscodes) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<Alert variant="erfolg">
|
||||
<ShieldCheck aria-hidden="true" />
|
||||
<AlertTitle>Zweiter Faktor ist aktiv</AlertTitle>
|
||||
<AlertDescription>
|
||||
Ab jetzt brauchst du bei jeder Anmeldung den Code aus deiner App.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold">Deine Wiederherstellungscodes</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Der einzige Weg zurück, wenn das Handy mit der App verloren geht. Jeder Code
|
||||
funktioniert genau einmal. <strong>Sie werden dir nie wieder angezeigt.</strong> Druck
|
||||
sie aus und leg sie zu deinen Betreuungsunterlagen — nicht auf dasselbe Gerät.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="grid grid-cols-2 gap-2 rounded-md border bg-muted/50 p-4 font-mono text-sm">
|
||||
{zustand.wiederherstellungscodes.map((code) => (
|
||||
<li key={code} className="tracking-wide">
|
||||
{code}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
void navigator.clipboard
|
||||
.writeText(zustand.wiederherstellungscodes!.join('\n'))
|
||||
.then(() => {
|
||||
setKopiert(true)
|
||||
setTimeout(() => setKopiert(false), 2500)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{kopiert ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
|
||||
{kopiert ? 'Kopiert' : 'Codes kopieren'}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" className="flex-1" onClick={() => window.print()}>
|
||||
Drucken
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => router.push('/ersteinrichtung')}
|
||||
>
|
||||
Ich habe die Codes gesichert — weiter
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Einrichtung -------------------------------------------------------
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<ol className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<span className="font-medium text-foreground">1.</span> Installiere eine
|
||||
Authenticator-App — etwa <strong>Aegis</strong> (Android) oder{' '}
|
||||
<strong>2FAS</strong> (iOS). Beide sind quelloffen und kommen ohne Konto aus.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-foreground">2.</span> Scanne den Code oder tippe das
|
||||
Geheimnis ab.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-medium text-foreground">3.</span> Gib die sechs Ziffern ein, die
|
||||
die App anzeigt.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
{qrCode && (
|
||||
<div className="flex justify-center rounded-lg border bg-white p-4">
|
||||
{/* Der QR-Code entsteht serverseitig und ist als Data-URI eingebettet -
|
||||
kein externer Dienst, kein CDN. */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={qrCode} alt="QR-Code zum Einrichten der Authenticator-App" width={240} height={240} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-sm font-medium">Geheimnis zum Abtippen</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 overflow-x-auto rounded-md border bg-muted px-3 py-2.5 font-mono text-sm">
|
||||
{gruppiert(secret)}
|
||||
</code>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Geheimnis kopieren"
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(secret).then(() => {
|
||||
setKopiert(true)
|
||||
setTimeout(() => setKopiert(false), 2500)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{kopiert ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action={aktion} className="space-y-4" noValidate>
|
||||
{zustand.fehler && (
|
||||
<Alert variant="fehler">
|
||||
<AlertCircle aria-hidden="true" />
|
||||
<AlertDescription>{zustand.fehler}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Feld id="code" beschriftung="Sechsstelliger Code aus der App" pflicht>
|
||||
<Input
|
||||
name="code"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
maxLength={7}
|
||||
placeholder="123456"
|
||||
className="text-center font-mono text-lg tracking-[0.4em]"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</Feld>
|
||||
|
||||
<Absenden />
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { aktuellerBenutzer } from '@/lib/auth/sitzung'
|
||||
import { zweitenFaktorVorbereiten } from '@/server/actions/zwei-faktor'
|
||||
import { EinrichtungsFormular } from './formular'
|
||||
|
||||
export const metadata: Metadata = { title: 'Zweiten Faktor einrichten' }
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function ZweiFaktorEinrichtenSeite() {
|
||||
const benutzer = await aktuellerBenutzer()
|
||||
if (!benutzer) redirect('/anmelden')
|
||||
if (benutzer.totpAktiviertAm) redirect('/heute')
|
||||
|
||||
const daten = await zweitenFaktorVorbereiten()
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Schritt 2 von 3: Zweiter Faktor</CardTitle>
|
||||
<CardDescription>
|
||||
Hier liegen Gesundheits- und Vermögensdaten einer Person, die nicht selbst entscheiden
|
||||
kann. Ein Passwort allein reicht dafür nicht — der zweite Faktor ist Pflicht und lässt
|
||||
sich nicht abschalten.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EinrichtungsFormular qrCode={daten.qrCode} secret={daten.secret} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user