/** * Custom Login Endpoint mit Audit-Logging * * POST /api/auth/login * * Dieser Endpoint wrappet den nativen Payload-Login und loggt fehlgeschlagene * Login-Versuche im Audit-Log. Erfolgreiche Logins werden durch den * afterLogin-Hook in der Users-Collection geloggt. * * Security: * - Rate-Limiting: 5 Versuche pro 15 Minuten pro IP (Anti-Brute-Force) * - IP-Blocklist-Prüfung * - Audit-Logging für fehlgeschlagene Logins * * Body: * - email: string (erforderlich) * - password: string (erforderlich) */ import { getPayload } from 'payload' import configPromise from '@payload-config' import { NextRequest, NextResponse } from 'next/server' import { logLoginFailed, logRateLimit } from '@/lib/audit/audit-service' import { authLimiter, runApiGuards, } from '@/lib/security' import { asObject, requiredString, validateJsonBody, validationErrorResponse, type ApiValidationResult, } from '@/lib/validation' /** * Extrahiert Client-Informationen aus dem Request für Audit-Logging */ function getClientInfo(req: NextRequest, ipAddress?: string): { ipAddress: string; userAgent: string } { const userAgent = req.headers.get('user-agent') || 'unknown' return { ipAddress: ipAddress || 'unknown', userAgent } } interface LoginBody { email: string password: string } function validateLoginBody(input: unknown): ApiValidationResult { const objectResult = asObject(input) if (!objectResult.valid) { return objectResult as ApiValidationResult } const emailResult = requiredString(objectResult.data, 'email') const passwordResult = requiredString(objectResult.data, 'password') const issues = [ ...(emailResult.valid ? [] : emailResult.issues), ...(passwordResult.valid ? [] : passwordResult.issues), ] if (issues.length > 0) { return { valid: false, issues } } return { valid: true, data: { email: emailResult.data, password: passwordResult.data, }, } } export async function POST(req: NextRequest): Promise { try { const payload = await getPayload({ config: configPromise }) const guardResult = await runApiGuards(req, { endpoint: '/api/auth/login', blocklistOnly: true, csrf: 'browser', rateLimiter: authLimiter, rateLimitMax: 5, onRateLimit: async () => { await logRateLimit(payload, '/api/auth/login', undefined, undefined) }, }) if (!guardResult.ok) { return guardResult.response } const bodyResult = await validateJsonBody(req, validateLoginBody) if (!bodyResult.valid) { return validationErrorResponse(bodyResult.issues) } const { email, password } = bodyResult.data try { // Versuche Login über Payload const result = await payload.login({ collection: 'users', data: { email, password, }, }) if (!result.user) { throw new Error('Login returned no user') } // Erfolgreicher Login - afterLogin Hook hat bereits geloggt // Setze Cookie für die Session const response = NextResponse.json({ success: true, user: { id: result.user.id, email: result.user.email, }, message: 'Login erfolgreich', }) // Set the token cookie if (result.token) { response.cookies.set('payload-token', result.token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', // Token expiration (default 2 hours) maxAge: result.exp ? result.exp - Math.floor(Date.now() / 1000) : 7200, }) } return response } catch (loginError) { // Login fehlgeschlagen - Audit-Log erstellen mit vollem Context const errorMessage = loginError instanceof Error ? loginError.message : 'Unbekannter Fehler' // Bestimme den Grund für das Fehlschlagen let reason = 'Unbekannter Fehler' if (errorMessage.includes('not found') || errorMessage.includes('incorrect')) { reason = 'Ungültige Anmeldedaten' } else if (errorMessage.includes('locked')) { reason = 'Konto gesperrt' } else if (errorMessage.includes('disabled')) { reason = 'Konto deaktiviert' } else if (errorMessage.includes('verify')) { reason = 'E-Mail nicht verifiziert' } else { reason = errorMessage } // Client-Info für Audit-Log extrahieren const clientInfo = getClientInfo(req, guardResult.ip) // Audit-Log für fehlgeschlagenen Login mit vollem Client-Context await logLoginFailed(payload, email, reason, clientInfo) console.log( `[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`, ) return NextResponse.json( { success: false, error: 'Anmeldung fehlgeschlagen', // Keine detaillierten Infos aus Sicherheitsgründen }, { status: 401 }, ) } } catch (error) { console.error('[API:Auth] Login error:', error) return NextResponse.json( { error: 'Interner Serverfehler' }, { status: 500 }, ) } } /** * GET /api/auth/login * * Gibt API-Dokumentation zurück. */ export async function GET(): Promise { return NextResponse.json({ endpoint: '/api/auth/login', method: 'POST', description: 'Login endpoint with audit logging for failed attempts', body: { email: 'string (required)', password: 'string (required)', }, response: { success: { success: true, user: { id: 'number', email: 'string' }, message: 'string', }, error: { success: false, error: 'string', }, }, note: 'Successful logins are logged via afterLogin hook. Failed attempts are logged here.', }) }