diff --git a/src/hooks/auditAuthEvents.ts b/src/hooks/auditAuthEvents.ts index b611d64..31d3a1f 100644 --- a/src/hooks/auditAuthEvents.ts +++ b/src/hooks/auditAuthEvents.ts @@ -62,19 +62,61 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => { console.log(`[Audit:Auth] Logout for ${user.email}`) } +/** + * Extrahiert einen Header-Wert aus verschiedenen Header-Formaten + * + * Unterstützt: + * - Express/Node IncomingHttpHeaders (object mit lowercase keys) + * - Fetch API Headers (mit .get() Methode) + * - Request mit .get() Methode (Express-style) + */ +function getHeaderValue(req: PayloadRequest, headerName: string): string | undefined { + const lowerName = headerName.toLowerCase() + + // 1. Versuche req.get() (Express Request Methode) + if (typeof (req as { get?: (name: string) => string | undefined }).get === 'function') { + const value = (req as { get: (name: string) => string | undefined }).get(lowerName) + if (value) return value + } + + // 2. Versuche headers.get() (Fetch API Headers) + if (req.headers && typeof (req.headers as { get?: (name: string) => string | null }).get === 'function') { + const value = (req.headers as { get: (name: string) => string | null }).get(lowerName) + if (value) return value + } + + // 3. Direkter Zugriff auf headers object (IncomingHttpHeaders) + if (req.headers && typeof req.headers === 'object') { + const headers = req.headers as Record + const value = headers[lowerName] + if (typeof value === 'string') return value + if (Array.isArray(value) && value.length > 0) return value[0] + } + + return undefined +} + /** * Extrahiert Client-Info aus einem PayloadRequest + * + * Funktioniert mit: + * - PayloadRequest (Express-basiert mit IncomingHttpHeaders) + * - Next.js Request (Fetch API basiert) */ function extractClientInfo(req?: PayloadRequest): ClientInfo { - if (!req?.headers?.get) return {} + if (!req) 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 forwarded = getHeaderValue(req, 'x-forwarded-for') + const realIp = getHeaderValue(req, 'x-real-ip') - const userAgent = req.headers.get('user-agent') || undefined + let ipAddress: string | undefined + if (forwarded) { + ipAddress = forwarded.split(',')[0]?.trim() + } else if (realIp) { + ipAddress = realIp + } + + const userAgent = getHeaderValue(req, 'user-agent') return { ipAddress, userAgent } } diff --git a/src/lib/audit/audit-service.ts b/src/lib/audit/audit-service.ts index a7d4c07..62b40e8 100644 --- a/src/lib/audit/audit-service.ts +++ b/src/lib/audit/audit-service.ts @@ -50,21 +50,65 @@ export interface AuditLogInput { metadata?: Record } +/** + * Extrahiert einen Header-Wert aus verschiedenen Header-Formaten + * + * Unterstützt: + * - Express/Node IncomingHttpHeaders (object mit lowercase keys) + * - Fetch API Headers (mit .get() Methode) + * - Request mit .get() Methode (Express-style) + */ +function getHeaderValue( + req: PayloadRequest, + headerName: string, +): string | undefined { + const lowerName = headerName.toLowerCase() + + // 1. Versuche req.get() (Express Request Methode) + if (typeof (req as { get?: (name: string) => string | undefined }).get === 'function') { + const value = (req as { get: (name: string) => string | undefined }).get(lowerName) + if (value) return value + } + + // 2. Versuche headers.get() (Fetch API Headers) + if (req.headers && typeof (req.headers as { get?: (name: string) => string | null }).get === 'function') { + const value = (req.headers as { get: (name: string) => string | null }).get(lowerName) + if (value) return value + } + + // 3. Direkter Zugriff auf headers object (IncomingHttpHeaders) + if (req.headers && typeof req.headers === 'object') { + const headers = req.headers as Record + const value = headers[lowerName] + if (typeof value === 'string') return value + if (Array.isArray(value) && value.length > 0) return value[0] + } + + return undefined +} + /** * Extrahiert Client-Informationen aus dem Request + * + * Funktioniert mit: + * - PayloadRequest (Express-basiert mit IncomingHttpHeaders) + * - Next.js Request (Fetch API basiert) */ function getClientInfo(req?: PayloadRequest): { ipAddress?: string; userAgent?: string } { if (!req) return {} // IP-Adresse aus verschiedenen Headern extrahieren - 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) || - 'unknown' + const forwarded = getHeaderValue(req, 'x-forwarded-for') + const realIp = getHeaderValue(req, 'x-real-ip') - const userAgent = req.headers?.get?.('user-agent') || undefined + let ipAddress: string | undefined + if (forwarded) { + ipAddress = forwarded.split(',')[0]?.trim() + } else if (realIp) { + ipAddress = realIp + } + + const userAgent = getHeaderValue(req, 'user-agent') return { ipAddress, userAgent } } @@ -156,6 +200,46 @@ export interface ClientInfo { userAgent?: string } +/** + * Prüft ob ein Objekt ein Request ist (hat headers property) + * im Gegensatz zu einem einfachen ClientInfo-Objekt + */ +function isRequest(obj: unknown): obj is PayloadRequest { + if (!obj || typeof obj !== 'object') return false + + // Ein Request hat typischerweise headers, method, url etc. + // ClientInfo hat nur ipAddress und/oder userAgent + const asRecord = obj as Record + + // Wenn es headers hat, ist es ein Request + if ('headers' in asRecord) return true + + // Wenn es method oder url hat, ist es ein Request + if ('method' in asRecord || 'url' in asRecord) return true + + // Wenn es NUR ipAddress und/oder userAgent hat, ist es ClientInfo + const keys = Object.keys(asRecord) + const clientInfoKeys = ['ipAddress', 'userAgent'] + if (keys.every((key) => clientInfoKeys.includes(key))) return false + + // Im Zweifel als Request behandeln + return true +} + +/** + * Extrahiert ClientInfo aus einem Request oder gibt das übergebene ClientInfo zurück + */ +function extractClientInfo(reqOrClientInfo?: PayloadRequest | ClientInfo): ClientInfo { + if (!reqOrClientInfo) return {} + + if (isRequest(reqOrClientInfo)) { + return getClientInfo(reqOrClientInfo) + } + + // Es ist bereits ClientInfo + return reqOrClientInfo +} + /** * Loggt einen fehlgeschlagenen Login * @@ -170,18 +254,7 @@ export async function logLoginFailed( reason: 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') { - // Es ist ein PayloadRequest - extrahiere Client-Info - clientInfo = getClientInfo(reqOrClientInfo as PayloadRequest) - } else { - // Es ist direktes ClientInfo - clientInfo = reqOrClientInfo as ClientInfo - } - } + const clientInfo = extractClientInfo(reqOrClientInfo) await createAuditLog( payload, @@ -211,16 +284,7 @@ export async function logPasswordReset( 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 - } - } + const clientInfo = extractClientInfo(reqOrClientInfo) await createAuditLog( payload,