/** * POST /api/auth/register * * Lokale Registrierung für den eigenständigen Betrieb. Sie ist nur * verfügbar, wenn ALLOW_OPEN_REGISTRATION=true; der erste * Benutzer wird ADMIN, weitere offene Registrierungen werden * dann automatisch zu USER ohne Kalender (oder abgelehnt, je * nach Konfiguration). * * Sicherheit: * - Rate-Limit pro IP (Brute-Force-/Enumeration-Schutz). * - SERIALIZABLE-Transaktion: Der "erste Benutzer * wird ADMIN"-Wettlauf kann nicht mehrere Admins erzeugen. * - Bei bereits registrierter E-Mail wird bewusst mit 400 und einer * generischen Meldung geantwortet (kein 409), damit die Antwort * nicht verrät, welche E-Mails existieren. */ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db/client'; import { hashPassword } from '@/lib/auth/password'; import { createSessionToken, setSessionCookie } from '@/lib/auth/session'; import { registerSchema } from '@/lib/validation'; import { getConfig } from '@/lib/config'; import { errorResponse } from '@/lib/api/responses'; import { checkRateLimit, recordRateLimitHit, getClientIp, } from '@/lib/security/rate-limit'; import { withSerializableTx } from '@/lib/db/tx'; /** Maximal 10 Registrierungsversuche pro IP und Zeitfenster. */ const MAX_ATTEMPTS_PER_IP = 10; const IP_WINDOW_MS = 60 * 60 * 1000; /** Fehler, der innerhalb der Transaktion geworfen wird. */ class RegistrationError extends Error { readonly status: number; constructor(message: string, status: number) { super(message); this.name = 'RegistrationError'; this.status = status; } } export async function POST(request: NextRequest): Promise { try { const ip = getClientIp(request); if ( !checkRateLimit(`register:ip:${ip}`, MAX_ATTEMPTS_PER_IP, IP_WINDOW_MS) .allowed ) { return NextResponse.json( { error: 'Zu viele Anmeldeversuche. Bitte spaeter erneut.' }, { status: 429 }, ); } const body = registerSchema.parse(await request.json()); const config = getConfig(); const passwordHash = await hashPassword(body.password); const createdUser = await withSerializableTx(async (tx) => { if (!config.allowOpenRegistration) { throw new RegistrationError( 'Registrierung ist derzeit deaktiviert.', 403, ); } const userCount = await tx.user.count(); // Der allererste Benutzer ist Admin (Betriebs-Setup). Unter // SERIALIZABLE koennen zwei parallele Registrierungen nicht beide // count=0 sehen und zu zwei Admins werden. const role = userCount === 0 ? 'ADMIN' : 'USER'; return tx.user.create({ data: { email: body.email, passwordHash, role, ...(body.username ? { username: body.username } : {}), }, select: { id: true, email: true, role: true, username: true }, }); }); const token = await createSessionToken(createdUser); await setSessionCookie(token); return NextResponse.json( { user: { id: createdUser.id, email: createdUser.email, role: createdUser.role, username: createdUser.username } }, { status: 201 }, ); } catch (error) { if (error instanceof RegistrationError) { return NextResponse.json( { error: error.message }, { status: error.status }, ); } // Eindeutige E-Mail (P2002): bewusst generisch antworten, um keine // Account-Enumeration zu ermoeglichen (gleiche Schranke wie bei // unbekannten E-Mails im Login). if ( typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'P2002' ) { recordRateLimitHit(`register:ip:${getClientIp(request)}`, IP_WINDOW_MS); return NextResponse.json( { error: 'Registrierung nicht moeglich. E-Mail oder Benutzername ist bereits in Verwendung.', }, { status: 400 }, ); } return errorResponse(error); } }