Files
betreuungs-cockpit/src/app/(auth)/ersteinrichtung/zugang-formular.tsx
T
Florian Wilde 5b54d7bc5f Ersteinrichtung schliessen: Geheimnis und eine Sperre statt zweier Schritte
Die Aktion ist die einzige Stelle der Anwendung, an der jemand ohne
Anmeldung schreiben darf. Zwei Dinge fehlten.

Erstens stand die Seite zwischen dem ersten Start und dem Anlegen des
Zugangs fuer jeden offen: Wer sie in diesem Fenster fand, wurde
Administrator einer Anwendung mit Gesundheits- und Finanzdaten. Das
Fenster ist kurz, aber Zufall ist keine Zugriffskontrolle. Jetzt muss
ERSTEINRICHTUNG_GEHEIMNIS aus der .env mit eingegeben werden; verglichen
wird ueber Hashes in gleichbleibender Zeit, und ein falscher Wert
antwortet mit derselben Mindestdauer wie das Anmeldeformular.

Fehlt der Wert, ist die Einrichtung gesperrt statt offen. Das ist
Absicht: Eine Installation, bei der die Zeile vergessen wurde, soll eine
Meldung zeigen und nicht einrichtbar sein.

Zweitens waren Pruefung und Anlegen zwei Schritte. Beide laufen jetzt in
einer Transaktion, und die Sperrzeile mit fester Id entscheidet - eine
zweite gleichzeitige Anfrage scheitert am Primaerschluessel, bevor ein
zweiter Administrator entsteht.

Die Seite liest die Sperrzeile mit: Lag der erste Zugang im Papierkorb,
stand Schritt 1 sonst wieder offen. Der Seed setzt sie ebenfalls.

Refs #38
2026-08-24 08:03:49 +02:00

104 lines
2.9 KiB
TypeScript

'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="geheimnis"
beschriftung="Einrichtungsgeheimnis"
erklaerung="Der Wert von ERSTEINRICHTUNG_GEHEIMNIS aus der .env. Er verhindert, dass jemand
anders diesen Zugang anlegt, solange die Seite noch offensteht."
fehler={zustand.felder?.geheimnis}
pflicht
>
<Input
name="geheimnis"
type="password"
autoComplete="off"
spellCheck={false}
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>
)
}