mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 15:04:14 +00:00
fix: support Express IncomingHttpHeaders for client info extraction
- Add getHeaderValue() helper that works with multiple header formats: - Express req.get() method - Fetch API headers.get() method - Direct IncomingHttpHeaders object access - Add isRequest() type guard to distinguish PayloadRequest from ClientInfo - Use extractClientInfo() helper for consistent request/ClientInfo handling - Apply same fix in auditAuthEvents.ts for hook context This fixes the issue where PayloadRequest objects were incorrectly detected as ClientInfo because IncomingHttpHeaders doesn't have .get() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
47d912016b
commit
0c0892f9de
2 changed files with 142 additions and 36 deletions
|
|
@ -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<string, string | string[] | undefined>
|
||||
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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,21 +50,65 @@ export interface AuditLogInput {
|
|||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, string | string[] | undefined>
|
||||
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<string, unknown>
|
||||
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
// 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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue