diff --git a/src/app/(payload)/api/auth/login/route.ts b/src/app/(payload)/api/auth/login/route.ts index dac3163..be394df 100644 --- a/src/app/(payload)/api/auth/login/route.ts +++ b/src/app/(payload)/api/auth/login/route.ts @@ -15,7 +15,21 @@ import { getPayload } from 'payload' import configPromise from '@payload-config' import { NextRequest, NextResponse } from 'next/server' -import { auditLoginFailed } from '@/hooks/auditAuthEvents' +import { logLoginFailed } from '@/lib/audit/audit-service' + +/** + * Extrahiert Client-Informationen aus dem Request für Audit-Logging + */ +function getClientInfo(req: NextRequest): { ipAddress: string; userAgent: string } { + const forwarded = req.headers.get('x-forwarded-for') + const realIp = req.headers.get('x-real-ip') + const ipAddress = + (forwarded ? forwarded.split(',')[0]?.trim() : undefined) || realIp || 'unknown' + + const userAgent = req.headers.get('user-agent') || 'unknown' + + return { ipAddress, userAgent } +} export async function POST(req: NextRequest): Promise { try { @@ -67,7 +81,7 @@ export async function POST(req: NextRequest): Promise { return response } catch (loginError) { - // Login fehlgeschlagen - Audit-Log erstellen + // Login fehlgeschlagen - Audit-Log erstellen mit vollem Context const errorMessage = loginError instanceof Error ? loginError.message : 'Unbekannter Fehler' @@ -79,12 +93,41 @@ export async function POST(req: NextRequest): Promise { 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 - await auditLoginFailed(payload, email, reason) + // Client-Info für Audit-Log + const clientInfo = getClientInfo(req) + + // Audit-Log für fehlgeschlagenen Login (ohne PayloadRequest) + await logLoginFailed(payload, email, reason) + + // Zusätzlich: Detailliertes Log mit IP/User-Agent direkt erstellen + try { + await (payload.create as Function)({ + collection: 'audit-logs', + data: { + action: 'login_failed', + severity: 'warning', + entityType: 'users', + userEmail: email, + ipAddress: clientInfo.ipAddress, + userAgent: clientInfo.userAgent, + description: `Fehlgeschlagener Login-Versuch für ${email}: ${reason}`, + metadata: { reason, source: '/api/auth/login' }, + }, + overrideAccess: true, + }) + } catch (auditError) { + console.error('[Auth:Login] Failed to create detailed audit log:', auditError) + } + + console.log( + `[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`, + ) return NextResponse.json( { diff --git a/src/app/(payload)/api/users/login/route.ts b/src/app/(payload)/api/users/login/route.ts new file mode 100644 index 0000000..1d0c8e9 --- /dev/null +++ b/src/app/(payload)/api/users/login/route.ts @@ -0,0 +1,150 @@ +/** + * Override für Payload's native Login-Route + * + * POST /api/users/login + * + * Diese Route überschreibt den nativen Payload-Login-Endpoint, um fehlgeschlagene + * Login-Versuche im Audit-Log zu erfassen. Dies ist notwendig, weil Payload + * keinen nativen afterLoginFailed Hook hat. + * + * Erfolgreiche Logins werden weiterhin durch den afterLogin-Hook geloggt. + */ + +import { getPayload } from 'payload' +import configPromise from '@payload-config' +import { NextRequest, NextResponse } from 'next/server' +import { logLoginFailed } from '@/lib/audit/audit-service' + +/** + * Extrahiert Client-Informationen aus dem Request für Audit-Logging + */ +function getClientInfo(req: NextRequest): { ipAddress: string; userAgent: string } { + const forwarded = req.headers.get('x-forwarded-for') + const realIp = req.headers.get('x-real-ip') + const ipAddress = + (forwarded ? forwarded.split(',')[0]?.trim() : undefined) || realIp || 'unknown' + + const userAgent = req.headers.get('user-agent') || 'unknown' + + return { ipAddress, userAgent } +} + +export async function POST(req: NextRequest): Promise { + const payload = await getPayload({ config: configPromise }) + + try { + const body = await req.json() + const { email, password } = body + + // Validierung + if (!email || !password) { + return NextResponse.json( + { + errors: [{ message: 'E-Mail und Passwort sind erforderlich' }], + }, + { status: 400 }, + ) + } + + try { + // Versuche Login über Payload + const result = await payload.login({ + collection: 'users', + data: { + email, + password, + }, + }) + + // Erfolgreicher Login - afterLogin Hook hat bereits geloggt + // Response im Payload-Format zurückgeben + const response = NextResponse.json({ + message: 'Auth Passed', + user: result.user, + token: result.token, + exp: result.exp, + }) + + // Set the token cookie (wie Payload es macht) + 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 - Audit-Log erstellen mit vollem Request-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 + const clientInfo = getClientInfo(req) + + // Audit-Log für fehlgeschlagenen Login mit IP und User-Agent + await logLoginFailed(payload, email, reason, undefined) + + // Zusätzlich: Manuelles Logging mit Client-Info da wir keinen PayloadRequest haben + try { + await (payload.create as Function)({ + collection: 'audit-logs', + data: { + action: 'login_failed', + severity: 'warning', + entityType: 'users', + userEmail: email, + ipAddress: clientInfo.ipAddress, + userAgent: clientInfo.userAgent, + description: `Fehlgeschlagener Login-Versuch für ${email}: ${reason}`, + metadata: { reason }, + }, + overrideAccess: true, + }) + } catch (auditError) { + console.error('[Auth:Login] Failed to create detailed audit log:', auditError) + } + + console.log( + `[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`, + ) + + // Response im Payload-Format (wie der native Endpoint) + return NextResponse.json( + { + errors: [ + { + message: 'The email or password provided is incorrect.', + }, + ], + }, + { status: 401 }, + ) + } + } catch (error) { + console.error('[API:Auth] Login error:', error) + return NextResponse.json( + { + errors: [{ message: 'Interner Serverfehler' }], + }, + { status: 500 }, + ) + } +} diff --git a/src/hooks/auditAuthEvents.ts b/src/hooks/auditAuthEvents.ts index c46d70f..02d2df5 100644 --- a/src/hooks/auditAuthEvents.ts +++ b/src/hooks/auditAuthEvents.ts @@ -60,28 +60,38 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => { * Hook: Loggt Passwort-Reset-Anfragen * * WICHTIG: Dieser Hook muss in Users.hooks.afterForgotPassword registriert werden + * + * Der Hook bekommt { args, collection, context } wobei: + * - args.req.payload = Payload-Instanz + * - args.data.email = E-Mail-Adresse + * - args.req = PayloadRequest (für IP/User-Agent) */ -export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({ - args, - context, -}) => { +export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({ args }) => { // Bei Forgot Password haben wir nur die E-Mail, nicht die User-ID // Aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert const email = args?.data?.email as string | undefined - // Payload-Instanz aus dem Context holen - const payload = (context as { req?: { payload?: Payload } })?.req?.payload + // Payload-Instanz und Request aus args holen (NICHT aus context!) + const req = args?.req as PayloadRequest | undefined + const payload = req?.payload if (payload && email) { - await createAuditLog(payload, { - action: 'password_reset', - severity: 'info', - entityType: 'users', - userEmail: email, - description: `Passwort-Reset angefordert für ${email}`, - }) + await createAuditLog( + payload, + { + action: 'password_reset', + severity: 'info', + entityType: 'users', + userEmail: email, + description: `Passwort-Reset angefordert für ${email}`, + }, + req, // Request für IP/User-Agent übergeben + ) console.log(`[Audit:Auth] Password reset requested for ${email}`) + } else { + // Fallback-Logging wenn Payload nicht verfügbar + console.warn(`[Audit:Auth] Could not log password reset - payload: ${!!payload}, email: ${email}`) } }