mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
fix: eliminate duplicate audit entries and add proper client context
- Extend logLoginFailed to accept ClientInfo directly (not just PayloadRequest) - Add logPasswordReset function for password reset audit logging - Remove duplicate manual payload.create calls in login routes - Implement real fallback in auditAfterForgotPassword with structured JSON log - Login routes now create single audit entry with full IP/User-Agent context 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
dfb35566b7
commit
47d912016b
4 changed files with 139 additions and 66 deletions
|
|
@ -99,31 +99,11 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||
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})`,
|
||||
|
|
|
|||
|
|
@ -96,31 +96,11 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||
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})`,
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
await logLoginFailed(payload, email, reason, req)
|
||||
await logLoginFailed(payload, email, reason, reqOrClientInfo)
|
||||
console.log(`[Audit:Auth] Login failed for ${email}: ${reason}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
}
|
||||
}
|
||||
|
||||
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<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
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue