diff --git a/src/app/(payload)/api/auth/login/route.ts b/src/app/(payload)/api/auth/login/route.ts index be394df..12c0d0b 100644 --- a/src/app/(payload)/api/auth/login/route.ts +++ b/src/app/(payload)/api/auth/login/route.ts @@ -99,31 +99,11 @@ export async function POST(req: NextRequest): Promise { reason = errorMessage } - // Client-Info für Audit-Log + // Client-Info für Audit-Log extrahieren 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) - } + // 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})`, diff --git a/src/app/(payload)/api/users/login/route.ts b/src/app/(payload)/api/users/login/route.ts index 1d0c8e9..7486c29 100644 --- a/src/app/(payload)/api/users/login/route.ts +++ b/src/app/(payload)/api/users/login/route.ts @@ -96,31 +96,11 @@ export async function POST(req: NextRequest): Promise { reason = errorMessage } - // Client-Info für Audit-Log + // Client-Info für Audit-Log extrahieren 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) - } + // 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})`, diff --git a/src/hooks/auditAuthEvents.ts b/src/hooks/auditAuthEvents.ts index 02d2df5..b611d64 100644 --- a/src/hooks/auditAuthEvents.ts +++ b/src/hooks/auditAuthEvents.ts @@ -11,7 +11,13 @@ import type { Payload, PayloadRequest, } from 'payload' -import { logLoginSuccess, logLoginFailed, createAuditLog } from '../lib/audit/audit-service' +import { + logLoginSuccess, + logLoginFailed, + logPasswordReset, + createAuditLog, + type ClientInfo, +} from '../lib/audit/audit-service' interface AuthUser { id: number @@ -56,6 +62,23 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => { console.log(`[Audit:Auth] Logout for ${user.email}`) } +/** + * Extrahiert Client-Info aus einem PayloadRequest + */ +function extractClientInfo(req?: PayloadRequest): ClientInfo { + if (!req?.headers?.get) return {} + + const forwarded = req.headers.get('x-forwarded-for') + const realIp = req.headers.get('x-real-ip') + const ipAddress = + (typeof forwarded === 'string' ? forwarded.split(',')[0]?.trim() : undefined) || + (typeof realIp === 'string' ? realIp : undefined) + + const userAgent = req.headers.get('user-agent') || undefined + + return { ipAddress, userAgent } +} + /** * Hook: Loggt Passwort-Reset-Anfragen * @@ -71,27 +94,46 @@ export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async // Aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert const email = args?.data?.email as string | undefined - // Payload-Instanz und Request aus args holen (NICHT aus context!) + if (!email) { + console.warn('[Audit:Auth] Password reset hook called without email') + return + } + + // Payload-Instanz und Request aus args holen 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}`, - }, - req, // Request für IP/User-Agent übergeben - ) + // Client-Info extrahieren (auch für Fallback-Log) + const clientInfo = extractClientInfo(req) + if (payload) { + // Normaler Pfad: Logging über Audit-Service + await logPasswordReset(payload, email, req) 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}`) + // Fallback: Strukturiertes Log wenn Payload nicht verfügbar + // Dies sollte nie passieren, aber wir loggen zumindest strukturiert + const fallbackEntry = { + timestamp: new Date().toISOString(), + action: 'password_reset', + severity: 'info', + entityType: 'users', + userEmail: email, + ipAddress: clientInfo.ipAddress || 'unknown', + userAgent: clientInfo.userAgent || 'unknown', + description: `Passwort-Reset angefordert für ${email}`, + fallback: true, + reason: 'Payload instance not available in hook context', + } + + // Strukturiertes JSON-Log für spätere Analyse/Import + console.error('[Audit:Auth:Fallback]', JSON.stringify(fallbackEntry)) + + // Zusätzlich lesbare Warnung + console.warn( + `[Audit:Auth] FALLBACK: Password reset for ${email} logged to console only ` + + `(IP: ${clientInfo.ipAddress || 'unknown'})`, + ) } } @@ -104,13 +146,18 @@ export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async * Verwendung: * - In Custom-API-Routes wenn Login fehlschlägt * - In Middleware die Auth-Errors abfängt + * + * @param payload - Payload-Instanz + * @param email - E-Mail des fehlgeschlagenen Login-Versuchs + * @param reason - Grund für das Fehlschlagen + * @param reqOrClientInfo - PayloadRequest ODER direktes ClientInfo-Objekt */ export async function auditLoginFailed( payload: Payload, email: string, reason: string, - req?: PayloadRequest, + reqOrClientInfo?: PayloadRequest | ClientInfo, ): Promise { - await logLoginFailed(payload, email, reason, req) + await logLoginFailed(payload, email, reason, reqOrClientInfo) console.log(`[Audit:Auth] Login failed for ${email}: ${reason}`) } diff --git a/src/lib/audit/audit-service.ts b/src/lib/audit/audit-service.ts index c026eca..a7d4c07 100644 --- a/src/lib/audit/audit-service.ts +++ b/src/lib/audit/audit-service.ts @@ -148,15 +148,41 @@ export async function logLoginSuccess( ) } +/** + * Client-Info Interface für direkte Übergabe (wenn kein PayloadRequest verfügbar) + */ +export interface ClientInfo { + ipAddress?: string + userAgent?: string +} + /** * Loggt einen fehlgeschlagenen Login + * + * @param payload - Payload-Instanz + * @param email - E-Mail des Login-Versuchs + * @param reason - Grund für das Fehlschlagen + * @param reqOrClientInfo - PayloadRequest ODER direktes ClientInfo-Objekt */ export async function logLoginFailed( payload: Payload, email: string, reason: string, - req?: PayloadRequest, + reqOrClientInfo?: PayloadRequest | ClientInfo, ): Promise { + // Unterscheide zwischen PayloadRequest und direktem ClientInfo + let clientInfo: ClientInfo = {} + + if (reqOrClientInfo) { + if ('headers' in reqOrClientInfo && typeof reqOrClientInfo.headers?.get === 'function') { + // Es ist ein PayloadRequest - extrahiere Client-Info + clientInfo = getClientInfo(reqOrClientInfo as PayloadRequest) + } else { + // Es ist direktes ClientInfo + clientInfo = reqOrClientInfo as ClientInfo + } + } + await createAuditLog( payload, { @@ -164,10 +190,50 @@ export async function logLoginFailed( severity: 'warning', entityType: 'users', userEmail: email, + ipAddress: clientInfo.ipAddress, + userAgent: clientInfo.userAgent, description: `Fehlgeschlagener Login-Versuch für ${email}: ${reason}`, metadata: { reason }, }, - req, + undefined, // Kein req mehr nötig, Client-Info ist bereits im Input + ) +} + +/** + * Loggt eine Passwort-Reset-Anfrage + * + * @param payload - Payload-Instanz + * @param email - E-Mail für den Reset + * @param reqOrClientInfo - PayloadRequest ODER direktes ClientInfo-Objekt + */ +export async function logPasswordReset( + payload: Payload, + email: string, + reqOrClientInfo?: PayloadRequest | ClientInfo, +): Promise { + // Unterscheide zwischen PayloadRequest und direktem ClientInfo + let clientInfo: ClientInfo = {} + + if (reqOrClientInfo) { + if ('headers' in reqOrClientInfo && typeof reqOrClientInfo.headers?.get === 'function') { + clientInfo = getClientInfo(reqOrClientInfo as PayloadRequest) + } else { + clientInfo = reqOrClientInfo as ClientInfo + } + } + + await createAuditLog( + payload, + { + action: 'password_reset', + severity: 'info', + entityType: 'users', + userEmail: email, + ipAddress: clientInfo.ipAddress, + userAgent: clientInfo.userAgent, + description: `Passwort-Reset angefordert für ${email}`, + }, + undefined, ) }