Files
bricktrack/src/app/(auth)/einladung/[token]/page.tsx
T
unknownandClaude Opus 5 52d69e5654 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>
2026-09-07 07:01:55 +02:00

55 lines
1.6 KiB
TypeScript

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>
);
}