mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +00:00
fix: complete auth event audit logging
Addresses remaining gaps from the audit review: 1. Register afterForgotPassword hook in Users collection - Password reset requests are now properly logged - Fixed hook signature (uses context instead of req) 2. Create custom /api/auth/login endpoint - Wraps native Payload login - Logs failed login attempts via auditLoginFailed - Returns proper error responses without exposing details 3. Export auditLoginFailed helper function - Can be used by other custom auth handlers - Calls logLoginFailed from audit-service Now all critical auth events are tracked: - Successful logins (afterLogin hook) - Failed logins (custom /api/auth/login endpoint) - Logouts (afterLogout hook) - Password reset requests (afterForgotPassword hook) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f667792ba7
commit
7b8efcff38
3 changed files with 179 additions and 12 deletions
134
src/app/(payload)/api/auth/login/route.ts
Normal file
134
src/app/(payload)/api/auth/login/route.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* Custom Login Endpoint mit Audit-Logging
|
||||
*
|
||||
* POST /api/auth/login
|
||||
*
|
||||
* Dieser Endpoint wrappet den nativen Payload-Login und loggt fehlgeschlagene
|
||||
* Login-Versuche im Audit-Log. Erfolgreiche Logins werden durch den
|
||||
* afterLogin-Hook in der Users-Collection geloggt.
|
||||
*
|
||||
* Body:
|
||||
* - email: string (erforderlich)
|
||||
* - password: string (erforderlich)
|
||||
*/
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auditLoginFailed } from '@/hooks/auditAuthEvents'
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const body = await req.json()
|
||||
|
||||
const { email, password } = body
|
||||
|
||||
// Validierung
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: '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
|
||||
// Setze Cookie für die Session
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: result.user.id,
|
||||
email: result.user.email,
|
||||
},
|
||||
message: 'Login erfolgreich',
|
||||
})
|
||||
|
||||
// Set the token cookie
|
||||
if (result.token) {
|
||||
response.cookies.set('payload-token', result.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
// Token expiration (default 2 hours)
|
||||
maxAge: result.exp ? result.exp - Math.floor(Date.now() / 1000) : 7200,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (loginError) {
|
||||
// Login fehlgeschlagen - Audit-Log erstellen
|
||||
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 {
|
||||
reason = errorMessage
|
||||
}
|
||||
|
||||
// Audit-Log für fehlgeschlagenen Login
|
||||
await auditLoginFailed(payload, email, reason)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Anmeldung fehlgeschlagen',
|
||||
// Keine detaillierten Infos aus Sicherheitsgründen
|
||||
},
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[API:Auth] Login error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Interner Serverfehler' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/login
|
||||
*
|
||||
* Gibt API-Dokumentation zurück.
|
||||
*/
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return NextResponse.json({
|
||||
endpoint: '/api/auth/login',
|
||||
method: 'POST',
|
||||
description: 'Login endpoint with audit logging for failed attempts',
|
||||
body: {
|
||||
email: 'string (required)',
|
||||
password: 'string (required)',
|
||||
},
|
||||
response: {
|
||||
success: {
|
||||
success: true,
|
||||
user: { id: 'number', email: 'string' },
|
||||
message: 'string',
|
||||
},
|
||||
error: {
|
||||
success: false,
|
||||
error: 'string',
|
||||
},
|
||||
},
|
||||
note: 'Successful logins are logged via afterLogin hook. Failed attempts are logged here.',
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import type { CollectionConfig } from 'payload'
|
||||
import { auditUserAfterChange, auditUserAfterDelete } from '../hooks/auditUserChanges'
|
||||
import { auditAfterLogin, auditAfterLogout } from '../hooks/auditAuthEvents'
|
||||
import {
|
||||
auditAfterLogin,
|
||||
auditAfterLogout,
|
||||
auditAfterForgotPassword,
|
||||
} from '../hooks/auditAuthEvents'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
|
|
@ -13,6 +17,7 @@ export const Users: CollectionConfig = {
|
|||
afterDelete: [auditUserAfterDelete],
|
||||
afterLogin: [auditAfterLogin],
|
||||
afterLogout: [auditAfterLogout],
|
||||
afterForgotPassword: [auditAfterForgotPassword],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import type {
|
|||
CollectionAfterLoginHook,
|
||||
CollectionAfterLogoutHook,
|
||||
CollectionAfterForgotPasswordHook,
|
||||
Payload,
|
||||
PayloadRequest,
|
||||
} from 'payload'
|
||||
import { logLoginSuccess, createAuditLog } from '../lib/audit/audit-service'
|
||||
import { logLoginSuccess, logLoginFailed, createAuditLog } from '../lib/audit/audit-service'
|
||||
|
||||
interface AuthUser {
|
||||
id: number
|
||||
|
|
@ -56,23 +58,49 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => {
|
|||
|
||||
/**
|
||||
* Hook: Loggt Passwort-Reset-Anfragen
|
||||
*
|
||||
* WICHTIG: Dieser Hook muss in Users.hooks.afterForgotPassword registriert werden
|
||||
*/
|
||||
export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({ args, context }) => {
|
||||
// Hinweis: Bei Forgot Password haben wir nur die E-Mail, nicht die User-ID
|
||||
// aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert
|
||||
export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({
|
||||
args,
|
||||
context,
|
||||
}) => {
|
||||
// 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
|
||||
|
||||
// Wir loggen nur wenn der Context die Payload-Instanz enthält
|
||||
if (context && 'payload' in context) {
|
||||
const payload = context.payload as typeof import('payload').default
|
||||
// Payload-Instanz aus dem Context holen
|
||||
const payload = (context as { req?: { payload?: Payload } })?.req?.payload
|
||||
|
||||
await createAuditLog(payload as unknown as import('payload').Payload, {
|
||||
if (payload && email) {
|
||||
await createAuditLog(payload, {
|
||||
action: 'password_reset',
|
||||
severity: 'info',
|
||||
entityType: 'users',
|
||||
userEmail: args.data?.email as string || 'unknown',
|
||||
description: `Passwort-Reset angefordert für ${args.data?.email || 'unknown'}`,
|
||||
userEmail: email,
|
||||
description: `Passwort-Reset angefordert für ${email}`,
|
||||
})
|
||||
|
||||
console.log(`[Audit:Auth] Password reset requested for ${args.data?.email}`)
|
||||
console.log(`[Audit:Auth] Password reset requested for ${email}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion: Loggt fehlgeschlagene Logins
|
||||
*
|
||||
* Diese Funktion kann von Custom-Auth-Handlern oder Middleware aufgerufen werden,
|
||||
* da Payload keinen nativen afterLoginFailed Hook hat.
|
||||
*
|
||||
* Verwendung:
|
||||
* - In Custom-API-Routes wenn Login fehlschlägt
|
||||
* - In Middleware die Auth-Errors abfängt
|
||||
*/
|
||||
export async function auditLoginFailed(
|
||||
payload: Payload,
|
||||
email: string,
|
||||
reason: string,
|
||||
req?: PayloadRequest,
|
||||
): Promise<void> {
|
||||
await logLoginFailed(payload, email, reason, req)
|
||||
console.log(`[Audit:Auth] Login failed for ${email}: ${reason}`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue