feat(auth): better-auth mit passwort, passkeys und app-shell
Registrierung des ersten Nutzers macht ihn zum Administrator, danach ist die Registrierung geschlossen (Einladungen folgen in Phase 2). Login per Passwort oder Passkey inklusive Conditional UI, Verwaltung der Passkeys unter "Konto" mit Umbenennen und Loeschen. Rate-Limiting liegt in der Datenbank und ueberlebt Neustarts; Session-Cookies sind httpOnly, sameSite=lax und im Produktivbetrieb secure. Die Client-IP wird hinter dem Reverse Proxy aus x-forwarded-for gelesen. Dazu die mobile-first Grundstruktur: Bottom-Navigation, Header mit Konto-Menue, Healthcheck unter /api/health und ein Dashboard mit den vier Einstiegspunkten. Bedienelemente sind auf 44px Touch-Groesse angehoben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { headers } from 'next/headers';
|
||||
import { PasskeyManager, type PasskeyRow } from '@/components/auth/passkey-manager';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { auth } from '@/lib/auth';
|
||||
import { requireUser } from '@/lib/session';
|
||||
|
||||
export const metadata: Metadata = { title: 'Konto' };
|
||||
|
||||
export default async function AccountPage() {
|
||||
const user = await requireUser();
|
||||
|
||||
const passkeys = await auth.api.listPasskeys({ headers: await headers() });
|
||||
const rows: PasskeyRow[] = passkeys.map((passkey) => ({
|
||||
id: passkey.id,
|
||||
name: passkey.name ?? null,
|
||||
createdAt: passkey.createdAt ? new Date(passkey.createdAt) : null,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Konto</h1>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil</CardTitle>
|
||||
<CardDescription>Diese Angaben stammen aus deiner Registrierung.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">Name</span>
|
||||
<span className="font-medium">{user.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">E-Mail</span>
|
||||
<span className="truncate font-medium">{user.email}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-muted-foreground">Rolle</span>
|
||||
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
|
||||
{user.role === 'admin' ? 'Administrator' : 'Mitglied'}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Passkeys</CardTitle>
|
||||
<CardDescription>
|
||||
Passkeys sind an diese Domain gebunden und funktionieren nur über HTTPS. Du kannst
|
||||
beliebig viele Geräte hinterlegen.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PasskeyManager passkeys={rows} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user