/** * Custom Login Route mit Audit-Logging * * POST /api/users/login * * Diese Route erweitert Payload's native Login um: * - Audit-Logging für fehlgeschlagene Login-Versuche * - Rate-Limiting (Anti-Brute-Force) * - IP-Blocklist-Prüfung * - CSRF-Schutz für Browser-Requests * * Wichtig: Jedes Security-Feature ist in try-catch gewrappt, * damit ein Fehler in einem Feature nicht den gesamten Login blockiert. */ import { getPayload } from 'payload' import config from '@payload-config' import { NextRequest, NextResponse } from 'next/server' // Lazy imports für Security-Module um Initialisierungsfehler zu vermeiden type SecurityModules = { authLimiter?: { check: (ip: string) => Promise<{ allowed: boolean; remaining: number; resetIn: number }> } rateLimitHeaders?: (result: unknown, max: number) => Record getClientIpFromRequest?: (req: NextRequest) => string isIpBlocked?: (ip: string) => boolean validateCsrf?: (req: NextRequest) => { valid: boolean; reason?: string } } let securityModules: SecurityModules = {} let securityModulesLoaded = false async function getSecurityModules(): Promise { if (securityModulesLoaded) return securityModules try { const security = await import('@/lib/security') securityModules = { authLimiter: security.authLimiter, rateLimitHeaders: security.rateLimitHeaders as SecurityModules['rateLimitHeaders'], getClientIpFromRequest: security.getClientIpFromRequest, isIpBlocked: security.isIpBlocked, validateCsrf: security.validateCsrf, } } catch (err) { console.warn('[Login] Security modules not available:', err) securityModules = {} } securityModulesLoaded = true return securityModules } // Lazy import für Audit-Service type AuditService = { logLoginFailed?: ( payload: unknown, email: string, reason: string, clientInfo: { ipAddress: string; userAgent: string }, ) => Promise logRateLimit?: ( payload: unknown, endpoint: string, userId?: number, tenantId?: number, ) => Promise } let auditService: AuditService = {} let auditServiceLoaded = false async function getAuditService(): Promise { if (auditServiceLoaded) return auditService try { const audit = await import('@/lib/audit/audit-service') auditService = { logLoginFailed: audit.logLoginFailed as AuditService['logLoginFailed'], logRateLimit: audit.logRateLimit as AuditService['logRateLimit'], } } catch (err) { console.warn('[Login] Audit service not available:', err) auditService = {} } auditServiceLoaded = true return auditService } /** * Extrahiert Client-IP aus Request */ function getClientIp(req: NextRequest): string { const forwarded = req.headers.get('x-forwarded-for') const realIp = req.headers.get('x-real-ip') return (forwarded ? forwarded.split(',')[0]?.trim() : undefined) || realIp || 'unknown' } /** * Extrahiert Client-Informationen für Audit-Logging */ function getClientInfo(req: NextRequest): { ipAddress: string; userAgent: string } { return { ipAddress: getClientIp(req), userAgent: req.headers.get('user-agent') || 'unknown', } } export async function POST(req: NextRequest): Promise { const clientIp = getClientIp(req) // 1. IP-Blocklist prüfen (optional, fail-safe) try { const security = await getSecurityModules() if (security.isIpBlocked?.(clientIp)) { return NextResponse.json( { errors: [{ message: 'Access denied' }] }, { status: 403 }, ) } } catch (err) { console.warn('[Login] IP blocklist check failed:', err) // Continue - don't block login if check fails } // 2. CSRF-Schutz (nur für Browser-Requests, fail-safe) // API-Requests ohne Origin-Header (CLI, Server-to-Server) brauchen kein CSRF const origin = req.headers.get('origin') const isApiRequest = !origin && req.headers.get('content-type')?.includes('application/json') if (!isApiRequest) { try { const security = await getSecurityModules() if (security.validateCsrf) { const csrfResult = security.validateCsrf(req) if (!csrfResult.valid) { console.log('[Login] CSRF validation failed:', csrfResult.reason) return NextResponse.json( { errors: [{ message: 'CSRF validation failed' }] }, { status: 403 }, ) } } } catch (err) { console.warn('[Login] CSRF check failed:', err) // Continue - don't block login if check fails } } // 3. Rate-Limiting prüfen (optional, fail-safe) try { const security = await getSecurityModules() if (security.authLimiter) { const rateLimit = await security.authLimiter.check(clientIp) if (!rateLimit.allowed) { // Optionally log rate limit hit try { const payload = await getPayload({ config }) const audit = await getAuditService() await audit.logRateLimit?.(payload, '/api/users/login', undefined, undefined) } catch { // Ignore audit logging errors } const headers = security.rateLimitHeaders?.(rateLimit, 5) || {} return NextResponse.json( { errors: [{ message: 'Too many login attempts. Please try again later.' }] }, { status: 429, headers }, ) } } } catch (err) { console.warn('[Login] Rate limiting check failed:', err) // Continue - don't block login if check fails } // 4. Parse Request Body let email: string | undefined let password: string | undefined try { const contentType = req.headers.get('content-type') || '' if (contentType.includes('multipart/form-data')) { const formData = await req.formData() // Payload Admin Panel sendet Daten als _payload JSON-Feld const payloadField = formData.get('_payload') if (payloadField) { try { const payloadData = JSON.parse(payloadField.toString()) email = payloadData.email password = payloadData.password } catch { // Falls _payload kein gültiges JSON ist } } // Fallback: Direkte FormData-Felder if (!email || !password) { email = formData.get('email')?.toString() password = formData.get('password')?.toString() } } else if (contentType.includes('application/x-www-form-urlencoded')) { const formData = await req.formData() email = formData.get('email')?.toString() password = formData.get('password')?.toString() } else { // Default: JSON const body = await req.json() email = body.email password = body.password } } catch (err) { console.error('[Login] Failed to parse request body:', err) return NextResponse.json( { errors: [{ message: 'Invalid request body' }] }, { status: 400 }, ) } // 5. Validierung if (!email || !password) { return NextResponse.json( { errors: [{ message: 'E-Mail und Passwort sind erforderlich' }] }, { status: 400 }, ) } // 6. Login durchführen try { const payload = await getPayload({ config }) const result = await payload.login({ collection: 'users', data: { email, password }, }) // Erfolgreicher Login - Response erstellen const response = NextResponse.json({ message: 'Auth Passed', user: result.user, token: result.token, exp: result.exp, }) // Cookie setzen if (result.token) { response.cookies.set('payload-token', result.token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: result.exp ? result.exp - Math.floor(Date.now() / 1000) : 7200, }) } return response } catch (loginError) { // Login fehlgeschlagen const errorMessage = loginError instanceof Error ? loginError.message : 'Unknown error' // 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 } // Audit-Log für fehlgeschlagenen Login (optional, fail-safe) try { const payload = await getPayload({ config }) const audit = await getAuditService() const clientInfo = getClientInfo(req) await audit.logLoginFailed?.(payload, email, reason, clientInfo) console.log(`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`) } catch (auditErr) { console.warn('[Login] Audit logging failed:', auditErr) // Continue - don't let audit failure affect response } // Standard Payload-Fehlerantwort return NextResponse.json( { errors: [{ message: 'The email or password provided is incorrect.' }] }, { status: 401 }, ) } }