feat(einladungen): admin erzeugt einmal gueltige registrierungslinks
Nach dem ersten Nutzer bleibt die Registrierung geschlossen; weitere Konten entstehen nur ueber einen Einladungslink. Der Klartext-Token wird genau einmal angezeigt, in der Datenbank liegt sein SHA-256-Hash. Optional laesst sich eine Einladung an eine E-Mail-Adresse binden, die Rolle und die Gueltigkeit sind waehlbar. Die Freigabe zum Anlegen eines Kontos laeuft ueber AsyncLocalStorage: Der Server-Action, der den Token geprueft hat, umschliesst damit den signUp-Aufruf, und nur in diesem Kontext laesst der user-create-Hook von better-auth die Registrierung zu. Die Rolle kommt aus der Einladung und kann nicht ueber das Formular gesetzt werden. Der Smoke-Test deckt den ganzen Weg ab: Admin erstellt die Einladung, der zweite Elternteil registriert sich damit, bekommt die Rolle "Mitglied" (und wird von der Einladungsverwaltung weggeleitet), und der Link funktioniert kein zweites Mal. Refs Phase 2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { InviteForm } from '@/components/auth/invite-form';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { formatDate } from '@/lib/format';
|
||||
import { findUsableInvite } from '@/lib/invites';
|
||||
import { getSession } from '@/lib/session';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Einladung',
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default async function InvitePage({ params }: { params: Promise<{ token: string }> }) {
|
||||
if (await getSession()) redirect('/');
|
||||
|
||||
const { token } = await params;
|
||||
const invitation = findUsableInvite(token);
|
||||
|
||||
if (!invitation) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Einladung ungültig</CardTitle>
|
||||
<CardDescription>
|
||||
Dieser Link ist abgelaufen oder wurde schon benutzt. Lass dir einfach einen neuen
|
||||
schicken.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Link href="/anmelden" className="text-sm underline underline-offset-4">
|
||||
Zur Anmeldung
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Konto anlegen</CardTitle>
|
||||
<CardDescription>
|
||||
Du wurdest zu Bricktrack eingeladen. Der Link gilt noch bis zum{' '}
|
||||
{formatDate(invitation.expiresAt)}.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<InviteForm token={token} email={invitation.email} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user