mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +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 type { CollectionConfig } from 'payload'
|
||||||
import { auditUserAfterChange, auditUserAfterDelete } from '../hooks/auditUserChanges'
|
import { auditUserAfterChange, auditUserAfterDelete } from '../hooks/auditUserChanges'
|
||||||
import { auditAfterLogin, auditAfterLogout } from '../hooks/auditAuthEvents'
|
import {
|
||||||
|
auditAfterLogin,
|
||||||
|
auditAfterLogout,
|
||||||
|
auditAfterForgotPassword,
|
||||||
|
} from '../hooks/auditAuthEvents'
|
||||||
|
|
||||||
export const Users: CollectionConfig = {
|
export const Users: CollectionConfig = {
|
||||||
slug: 'users',
|
slug: 'users',
|
||||||
|
|
@ -13,6 +17,7 @@ export const Users: CollectionConfig = {
|
||||||
afterDelete: [auditUserAfterDelete],
|
afterDelete: [auditUserAfterDelete],
|
||||||
afterLogin: [auditAfterLogin],
|
afterLogin: [auditAfterLogin],
|
||||||
afterLogout: [auditAfterLogout],
|
afterLogout: [auditAfterLogout],
|
||||||
|
afterForgotPassword: [auditAfterForgotPassword],
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ import type {
|
||||||
CollectionAfterLoginHook,
|
CollectionAfterLoginHook,
|
||||||
CollectionAfterLogoutHook,
|
CollectionAfterLogoutHook,
|
||||||
CollectionAfterForgotPasswordHook,
|
CollectionAfterForgotPasswordHook,
|
||||||
|
Payload,
|
||||||
|
PayloadRequest,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
import { logLoginSuccess, createAuditLog } from '../lib/audit/audit-service'
|
import { logLoginSuccess, logLoginFailed, createAuditLog } from '../lib/audit/audit-service'
|
||||||
|
|
||||||
interface AuthUser {
|
interface AuthUser {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -56,23 +58,49 @@ export const auditAfterLogout: CollectionAfterLogoutHook = async ({ req }) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook: Loggt Passwort-Reset-Anfragen
|
* Hook: Loggt Passwort-Reset-Anfragen
|
||||||
|
*
|
||||||
|
* WICHTIG: Dieser Hook muss in Users.hooks.afterForgotPassword registriert werden
|
||||||
*/
|
*/
|
||||||
export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({ args, context }) => {
|
export const auditAfterForgotPassword: CollectionAfterForgotPasswordHook = async ({
|
||||||
// Hinweis: Bei Forgot Password haben wir nur die E-Mail, nicht die User-ID
|
args,
|
||||||
// aus Sicherheitsgründen wird nicht offengelegt ob die E-Mail existiert
|
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
|
// Payload-Instanz aus dem Context holen
|
||||||
if (context && 'payload' in context) {
|
const payload = (context as { req?: { payload?: Payload } })?.req?.payload
|
||||||
const payload = context.payload as typeof import('payload').default
|
|
||||||
|
|
||||||
await createAuditLog(payload as unknown as import('payload').Payload, {
|
if (payload && email) {
|
||||||
|
await createAuditLog(payload, {
|
||||||
action: 'password_reset',
|
action: 'password_reset',
|
||||||
severity: 'info',
|
severity: 'info',
|
||||||
entityType: 'users',
|
entityType: 'users',
|
||||||
userEmail: args.data?.email as string || 'unknown',
|
userEmail: email,
|
||||||
description: `Passwort-Reset angefordert für ${args.data?.email || 'unknown'}`,
|
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