Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
changePasswordSelf,
|
||||
redeemInvitation,
|
||||
redeemPasswordReset,
|
||||
requestEmailChange,
|
||||
requestPasswordReset,
|
||||
type EmailChangeState,
|
||||
type RedeemResetState,
|
||||
type RequestResetState,
|
||||
type SelfChangeState,
|
||||
} from "@/server/actions/auth-recovery";
|
||||
|
||||
/**
|
||||
* SEC2 — Formulare des Passwort-Self-Service.
|
||||
*
|
||||
* Alle Rückmeldungen sind bewusst generisch gehalten (Enumeration-Schutz): das
|
||||
* Formular zeigt genau den Text, den die Server-Action liefert, und ergänzt ihn
|
||||
* nicht um Hinweise auf die Existenz eines Kontos.
|
||||
*/
|
||||
|
||||
const OK = "mt-4 rounded-lg bg-[rgba(57,192,127,0.14)] px-3 py-2 text-sm text-[var(--ok)]";
|
||||
const ERR = "mt-4 rounded-lg bg-[rgba(255,107,107,0.16)] px-3 py-2 text-sm text-[var(--risk)]";
|
||||
|
||||
export function ForgotPasswordForm({ domain }: { domain: "tenant" | "platform" }) {
|
||||
const [state, action, pending] = useActionState<RequestResetState, FormData>(
|
||||
requestPasswordReset,
|
||||
{ status: "idle" },
|
||||
);
|
||||
|
||||
if (state.status === "done") {
|
||||
return (
|
||||
<p role="status" className={OK}>
|
||||
{state.message}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={action} className="mt-6 space-y-4">
|
||||
<input type="hidden" name="domain" value={domain} />
|
||||
<div>
|
||||
<Label htmlFor="email">E-Mail-Adresse</Label>
|
||||
<Input id="email" name="email" type="email" required autoComplete="email" className="mt-1" />
|
||||
</div>
|
||||
{state.status === "error" && (
|
||||
<p role="alert" className={ERR}>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={pending}>
|
||||
{pending ? "Sende…" : "Link anfordern"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResetPasswordForm({
|
||||
token,
|
||||
domain,
|
||||
policyHint,
|
||||
loginPath,
|
||||
variant = "reset",
|
||||
}: {
|
||||
token: string;
|
||||
domain: "tenant" | "platform";
|
||||
policyHint: string;
|
||||
loginPath: string;
|
||||
/** "invite" nutzt den Einladungs-Flow (Option C, WS3) statt des Reset-Flows. */
|
||||
variant?: "reset" | "invite";
|
||||
}) {
|
||||
const [state, action, pending] = useActionState<RedeemResetState, FormData>(
|
||||
variant === "invite" ? redeemInvitation : redeemPasswordReset,
|
||||
{ status: "idle" },
|
||||
);
|
||||
|
||||
if (state.status === "done") {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<p role="status" className={OK}>
|
||||
{variant === "invite"
|
||||
? "Ihr Konto ist eingerichtet. Sie können sich jetzt anmelden."
|
||||
: "Ihr Passwort wurde gesetzt. Alle bisherigen Sitzungen sind abgemeldet."}
|
||||
</p>
|
||||
{/* Bewusst kein Auto-Login: der Nutzer meldet sich neu an (SEC2 §6). */}
|
||||
<Button nativeButton={false} render={<a href={loginPath} />} className="mt-4 w-full justify-center">
|
||||
Zur Anmeldung
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={action} className="mt-6 space-y-4">
|
||||
<input type="hidden" name="token" value={token} />
|
||||
<input type="hidden" name="domain" value={domain} />
|
||||
<div>
|
||||
<Label htmlFor="password">Neues Passwort</Label>
|
||||
<Input id="password" name="password" type="password" required autoComplete="new-password" className="mt-1" />
|
||||
<p className="mt-1 text-xs text-muted-foreground">{policyHint}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="confirm">Wiederholen</Label>
|
||||
<Input id="confirm" name="confirm" type="password" required autoComplete="new-password" className="mt-1" />
|
||||
</div>
|
||||
{state.status === "error" && (
|
||||
<p role="alert" className={ERR}>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={pending}>
|
||||
{pending ? "Speichere…" : variant === "invite" ? "Konto einrichten" : "Passwort setzen"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChangePasswordSelfForm({
|
||||
domain,
|
||||
policyHint,
|
||||
}: {
|
||||
domain: "tenant" | "platform";
|
||||
policyHint: string;
|
||||
}) {
|
||||
const [state, action, pending] = useActionState<SelfChangeState, FormData>(changePasswordSelf, {
|
||||
status: "idle",
|
||||
});
|
||||
|
||||
return (
|
||||
<form action={action} className="space-y-3">
|
||||
<input type="hidden" name="domain" value={domain} />
|
||||
<div>
|
||||
<Label htmlFor="current">Aktuelles Passwort</Label>
|
||||
<Input id="current" name="current" type="password" required autoComplete="current-password" className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="new-password">Neues Passwort</Label>
|
||||
<Input id="new-password" name="password" type="password" required autoComplete="new-password" className="mt-1" />
|
||||
<p className="mt-1 text-xs text-muted-foreground">{policyHint}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="new-confirm">Wiederholen</Label>
|
||||
<Input id="new-confirm" name="confirm" type="password" required autoComplete="new-password" className="mt-1" />
|
||||
</div>
|
||||
{state.status !== "idle" && (
|
||||
<p role="status" className={state.status === "ok" ? OK : ERR}>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending ? "Speichere…" : "Passwort ändern"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChangeEmailForm({
|
||||
domain,
|
||||
currentEmail,
|
||||
}: {
|
||||
domain: "tenant" | "platform";
|
||||
currentEmail: string;
|
||||
}) {
|
||||
const [state, action, pending] = useActionState<EmailChangeState, FormData>(requestEmailChange, {
|
||||
status: "idle",
|
||||
});
|
||||
|
||||
return (
|
||||
<form action={action} className="space-y-3">
|
||||
<input type="hidden" name="domain" value={domain} />
|
||||
<div>
|
||||
<Label htmlFor="newEmail">Neue E-Mail-Adresse</Label>
|
||||
<Input id="newEmail" name="newEmail" type="email" required className="mt-1" placeholder={currentEmail} />
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Die Änderung wird erst wirksam, wenn Sie den Link in der neuen Adresse bestätigen.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email-current">Aktuelles Passwort zur Bestätigung</Label>
|
||||
<Input id="email-current" name="current" type="password" required autoComplete="current-password" className="mt-1" />
|
||||
</div>
|
||||
{state.status !== "idle" && (
|
||||
<p role="status" className={state.status === "ok" ? OK : ERR}>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
<Button type="submit" variant="outline" disabled={pending}>
|
||||
{pending ? "Sende…" : "Bestätigungslink senden"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user