mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
fix: correct auth audit logging - use args.req.payload and override native login
- Fix afterForgotPassword hook to read payload from args.req.payload instead of context - Create /api/users/login route to override native Payload login endpoint - Add IP/User-Agent context to failed login audit entries - Update /api/auth/login with consistent client info logging 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7b8efcff38
commit
dfb35566b7
3 changed files with 220 additions and 17 deletions
|
|
@ -15,7 +15,21 @@
|
|||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auditLoginFailed } from '@/hooks/auditAuthEvents'
|
||||
import { logLoginFailed } from '@/lib/audit/audit-service'
|
||||
|
||||
/**
|
||||
* Extrahiert Client-Informationen aus dem Request für Audit-Logging
|
||||
*/
|
||||
function getClientInfo(req: NextRequest): { ipAddress: string; userAgent: string } {
|
||||
const forwarded = req.headers.get('x-forwarded-for')
|
||||
const realIp = req.headers.get('x-real-ip')
|
||||
const ipAddress =
|
||||
(forwarded ? forwarded.split(',')[0]?.trim() : undefined) || realIp || 'unknown'
|
||||
|
||||
const userAgent = req.headers.get('user-agent') || 'unknown'
|
||||
|
||||
return { ipAddress, userAgent }
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
|
|
@ -67,7 +81,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||
|
||||
return response
|
||||
} catch (loginError) {
|
||||
// Login fehlgeschlagen - Audit-Log erstellen
|
||||
// Login fehlgeschlagen - Audit-Log erstellen mit vollem Context
|
||||
const errorMessage =
|
||||
loginError instanceof Error ? loginError.message : 'Unbekannter Fehler'
|
||||
|
||||
|
|
@ -79,12 +93,41 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||
reason = 'Konto gesperrt'
|
||||
} else if (errorMessage.includes('disabled')) {
|
||||
reason = 'Konto deaktiviert'
|
||||
} else if (errorMessage.includes('verify')) {
|
||||
reason = 'E-Mail nicht verifiziert'
|
||||
} else {
|
||||
reason = errorMessage
|
||||
}
|
||||
|
||||
// Audit-Log für fehlgeschlagenen Login
|
||||
await auditLoginFailed(payload, email, reason)
|
||||
// Client-Info für Audit-Log
|
||||
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)
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`,
|
||||
)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
|
|
|||
150
src/app/(payload)/api/users/login/route.ts
Normal file
150
src/app/(payload)/api/users/login/route.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Override für Payload's native Login-Route
|
||||
*
|
||||
* POST /api/users/login
|
||||
*
|
||||
* Diese Route überschreibt den nativen Payload-Login-Endpoint, um fehlgeschlagene
|
||||
* Login-Versuche im Audit-Log zu erfassen. Dies ist notwendig, weil Payload
|
||||
* keinen nativen afterLoginFailed Hook hat.
|
||||
*
|
||||
* Erfolgreiche Logins werden weiterhin durch den afterLogin-Hook geloggt.
|
||||
*/
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { logLoginFailed } from '@/lib/audit/audit-service'
|
||||
|
||||
/**
|
||||
* Extrahiert Client-Informationen aus dem Request für Audit-Logging
|
||||
*/
|
||||
function getClientInfo(req: NextRequest): { ipAddress: string; userAgent: string } {
|
||||
const forwarded = req.headers.get('x-forwarded-for')
|
||||
const realIp = req.headers.get('x-real-ip')
|
||||
const ipAddress =
|
||||
(forwarded ? forwarded.split(',')[0]?.trim() : undefined) || realIp || 'unknown'
|
||||
|
||||
const userAgent = req.headers.get('user-agent') || 'unknown'
|
||||
|
||||
return { ipAddress, userAgent }
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { email, password } = body
|
||||
|
||||
// Validierung
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
errors: [{ message: 'E-Mail und Passwort sind erforderlich' }],
|
||||
},
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
// Versuche Login über Payload
|
||||
const result = await payload.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
})
|
||||
|
||||
// Erfolgreicher Login - afterLogin Hook hat bereits geloggt
|
||||
// Response im Payload-Format zurückgeben
|
||||
const response = NextResponse.json({
|
||||
message: 'Auth Passed',
|
||||
user: result.user,
|
||||
token: result.token,
|
||||
exp: result.exp,
|
||||
})
|
||||
|
||||
// Set the token cookie (wie Payload es macht)
|
||||
if (result.token) {
|
||||
response.cookies.set('payload-token', result.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: result.exp ? result.exp - Math.floor(Date.now() / 1000) : 7200,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (loginError) {
|
||||
// Login fehlgeschlagen - Audit-Log erstellen mit vollem Request-Context
|
||||
const errorMessage =
|
||||
loginError instanceof Error ? loginError.message : 'Unbekannter Fehler'
|
||||
|
||||
// Bestimme den Grund für das Fehlschlagen
|
||||
let reason = 'Unbekannter Fehler'
|
||||
if (errorMessage.includes('not found') || errorMessage.includes('incorrect')) {
|
||||
reason = 'Ungültige Anmeldedaten'
|
||||
} else if (errorMessage.includes('locked')) {
|
||||
reason = 'Konto gesperrt'
|
||||
} else if (errorMessage.includes('disabled')) {
|
||||
reason = 'Konto deaktiviert'
|
||||
} else if (errorMessage.includes('verify')) {
|
||||
reason = 'E-Mail nicht verifiziert'
|
||||
} else {
|
||||
reason = errorMessage
|
||||
}
|
||||
|
||||
// Client-Info für Audit-Log
|
||||
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)
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Audit:Auth] Login failed for ${email}: ${reason} (IP: ${clientInfo.ipAddress})`,
|
||||
)
|
||||
|
||||
// Response im Payload-Format (wie der native Endpoint)
|
||||
return NextResponse.json(
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
message: 'The email or password provided is incorrect.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[API:Auth] Login error:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
errors: [{ message: 'Interner Serverfehler' }],
|
||||
},
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -60,28 +60,38 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => {
|
|||
* Hook: Loggt Passwort-Reset-Anfragen
|
||||
*
|
||||
* WICHTIG: Dieser Hook muss in Users.hooks.afterForgotPassword registriert werden
|
||||
*
|
||||
* Der Hook bekommt { args, collection, context } wobei:
|
||||
* - args.req.payload = Payload-Instanz
|
||||
* - args.data.email = E-Mail-Adresse
|
||||
* - args.req = PayloadRequest (für IP/User-Agent)
|
||||
*/
|
||||
export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({
|
||||
args,
|
||||
context,
|
||||
}) => {
|
||||
export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({ args }) => {
|
||||
// Bei Forgot Password haben wir nur die E-Mail, nicht die User-ID
|
||||
// Aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert
|
||||
const email = args?.data?.email as string | undefined
|
||||
|
||||
// Payload-Instanz aus dem Context holen
|
||||
const payload = (context as { req?: { payload?: Payload } })?.req?.payload
|
||||
// Payload-Instanz und Request aus args holen (NICHT aus context!)
|
||||
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}`,
|
||||
})
|
||||
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
|
||||
)
|
||||
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue