mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +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
|
reason = errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-Info für Audit-Log
|
// Client-Info für Audit-Log extrahieren
|
||||||
const clientInfo = getClientInfo(req)
|
const clientInfo = getClientInfo(req)
|
||||||
|
|
||||||
// Audit-Log für fehlgeschlagenen Login (ohne PayloadRequest)
|
// Audit-Log für fehlgeschlagenen Login mit vollem Client-Context
|
||||||
await logLoginFailed(payload, email, reason)
|
await logLoginFailed(payload, email, reason, clientInfo)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`,
|
`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`,
|
||||||
|
|
|
||||||
|
|
@ -96,31 +96,11 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||||
reason = errorMessage
|
reason = errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-Info für Audit-Log
|
// Client-Info für Audit-Log extrahieren
|
||||||
const clientInfo = getClientInfo(req)
|
const clientInfo = getClientInfo(req)
|
||||||
|
|
||||||
// Audit-Log für fehlgeschlagenen Login mit IP und User-Agent
|
// Audit-Log für fehlgeschlagenen Login mit vollem Client-Context
|
||||||
await logLoginFailed(payload, email, reason, undefined)
|
await logLoginFailed(payload, email, reason, clientInfo)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`,
|
`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,13 @@ import type {
|
||||||
Payload,
|
Payload,
|
||||||
PayloadRequest,
|
PayloadRequest,
|
||||||
} from 'payload'
|
} 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 {
|
interface AuthUser {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -56,6 +62,23 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => {
|
||||||
console.log(`[Audit:Auth] Logout for ${user.email}`)
|
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
|
* 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
|
// Aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert
|
||||||
const email = args?.data?.email as string | undefined
|
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 req = args?.req as PayloadRequest | undefined
|
||||||
const payload = req?.payload
|
const payload = req?.payload
|
||||||
|
|
||||||
if (payload && email) {
|
// Client-Info extrahieren (auch für Fallback-Log)
|
||||||
await createAuditLog(
|
const clientInfo = extractClientInfo(req)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if (payload) {
|
||||||
|
// Normaler Pfad: Logging über Audit-Service
|
||||||
|
await logPasswordReset(payload, email, req)
|
||||||
console.log(`[Audit:Auth] Password reset requested for ${email}`)
|
console.log(`[Audit:Auth] Password reset requested for ${email}`)
|
||||||
} else {
|
} else {
|
||||||
// Fallback-Logging wenn Payload nicht verfügbar
|
// Fallback: Strukturiertes Log wenn Payload nicht verfügbar
|
||||||
console.warn(`[Audit:Auth] Could not log password reset - payload: ${!!payload}, email: ${email}`)
|
// 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:
|
* Verwendung:
|
||||||
* - In Custom-API-Routes wenn Login fehlschlägt
|
* - In Custom-API-Routes wenn Login fehlschlägt
|
||||||
* - In Middleware die Auth-Errors abfängt
|
* - 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(
|
export async function auditLoginFailed(
|
||||||
payload: Payload,
|
payload: Payload,
|
||||||
email: string,
|
email: string,
|
||||||
reason: string,
|
reason: string,
|
||||||
req?: PayloadRequest,
|
reqOrClientInfo?: PayloadRequest | ClientInfo,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await logLoginFailed(payload, email, reason, req)
|
await logLoginFailed(payload, email, reason, reqOrClientInfo)
|
||||||
console.log(`[Audit:Auth] Login failed for ${email}: ${reason}`)
|
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
|
* 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(
|
export async function logLoginFailed(
|
||||||
payload: Payload,
|
payload: Payload,
|
||||||
email: string,
|
email: string,
|
||||||
reason: string,
|
reason: string,
|
||||||
req?: PayloadRequest,
|
reqOrClientInfo?: PayloadRequest | ClientInfo,
|
||||||
): Promise<void> {
|
): 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(
|
await createAuditLog(
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
|
|
@ -164,10 +190,50 @@ export async function logLoginFailed(
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
entityType: 'users',
|
entityType: 'users',
|
||||||
userEmail: email,
|
userEmail: email,
|
||||||
|
ipAddress: clientInfo.ipAddress,
|
||||||
|
userAgent: clientInfo.userAgent,
|
||||||
description: `Fehlgeschlagener Login-Versuch für ${email}: ${reason}`,
|
description: `Fehlgeschlagener Login-Versuch für ${email}: ${reason}`,
|
||||||
metadata: { 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