mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14:12 +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}`)
|
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
|
* Extrahiert Client-Info aus einem PayloadRequest
|
||||||
|
*
|
||||||
|
* Funktioniert mit:
|
||||||
|
* - PayloadRequest (Express-basiert mit IncomingHttpHeaders)
|
||||||
|
* - Next.js Request (Fetch API basiert)
|
||||||
*/
|
*/
|
||||||
function extractClientInfo(req?: PayloadRequest): ClientInfo {
|
function extractClientInfo(req?: PayloadRequest): ClientInfo {
|
||||||
if (!req?.headers?.get) return {}
|
if (!req) return {}
|
||||||
|
|
||||||
const forwarded = req.headers.get('x-forwarded-for')
|
const forwarded = getHeaderValue(req, 'x-forwarded-for')
|
||||||
const realIp = req.headers.get('x-real-ip')
|
const realIp = getHeaderValue(req, '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
|
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 }
|
return { ipAddress, userAgent }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,21 +50,65 @@ export interface AuditLogInput {
|
||||||
metadata?: Record<string, unknown>
|
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
|
* 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 } {
|
function getClientInfo(req?: PayloadRequest): { ipAddress?: string; userAgent?: string } {
|
||||||
if (!req) return {}
|
if (!req) return {}
|
||||||
|
|
||||||
// IP-Adresse aus verschiedenen Headern extrahieren
|
// IP-Adresse aus verschiedenen Headern extrahieren
|
||||||
const forwarded = req.headers?.get?.('x-forwarded-for')
|
const forwarded = getHeaderValue(req, 'x-forwarded-for')
|
||||||
const realIp = req.headers?.get?.('x-real-ip')
|
const realIp = getHeaderValue(req, 'x-real-ip')
|
||||||
const ipAddress =
|
|
||||||
(typeof forwarded === 'string' ? forwarded.split(',')[0]?.trim() : undefined) ||
|
|
||||||
(typeof realIp === 'string' ? realIp : undefined) ||
|
|
||||||
'unknown'
|
|
||||||
|
|
||||||
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 }
|
return { ipAddress, userAgent }
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +200,46 @@ export interface ClientInfo {
|
||||||
userAgent?: string
|
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
|
* Loggt einen fehlgeschlagenen Login
|
||||||
*
|
*
|
||||||
|
|
@ -170,18 +254,7 @@ export async function logLoginFailed(
|
||||||
reason: string,
|
reason: string,
|
||||||
reqOrClientInfo?: PayloadRequest | ClientInfo,
|
reqOrClientInfo?: PayloadRequest | ClientInfo,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Unterscheide zwischen PayloadRequest und direktem ClientInfo
|
const clientInfo = extractClientInfo(reqOrClientInfo)
|
||||||
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(
|
await createAuditLog(
|
||||||
payload,
|
payload,
|
||||||
|
|
@ -211,16 +284,7 @@ export async function logPasswordReset(
|
||||||
email: string,
|
email: string,
|
||||||
reqOrClientInfo?: PayloadRequest | ClientInfo,
|
reqOrClientInfo?: PayloadRequest | ClientInfo,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Unterscheide zwischen PayloadRequest und direktem ClientInfo
|
const clientInfo = extractClientInfo(reqOrClientInfo)
|
||||||
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(
|
await createAuditLog(
|
||||||
payload,
|
payload,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue