mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
- 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>
177 lines
5.1 KiB
TypeScript
177 lines
5.1 KiB
TypeScript
/**
|
|
* 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 { 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 {
|
|
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 mit vollem 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 (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(
|
|
{
|
|
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.',
|
|
})
|
|
}
|