mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +00:00
Rate Limiting Integration: - Add authLimiter (5 attempts/15min) to both login routes for brute-force protection - Migrate search endpoints from local checkRateLimit to central searchLimiter - Add IP blocklist checks to auth and search endpoints Data Masking Integration: - Integrate maskObject/maskString from security module into audit-service - Auto-mask previousValue, newValue, metadata, and descriptions in audit logs - Use maskError for error logging Pre-commit Hook: - Add "prepare" script to package.json for automatic hook installation - Hook is now installed automatically on pnpm install Note: CSRF middleware is available but not enforced on API routes since Payload CMS uses JWT auth and has built-in CORS/CSRF protection in config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
195 lines
5.5 KiB
TypeScript
195 lines
5.5 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.
|
|
*
|
|
* Security:
|
|
* - Rate-Limiting: 5 Versuche pro 15 Minuten pro IP (Anti-Brute-Force)
|
|
* - IP-Blocklist-Prüfung
|
|
* - Audit-Logging für fehlgeschlagene Logins
|
|
*
|
|
* Body:
|
|
* - email: string (erforderlich)
|
|
* - password: string (erforderlich)
|
|
*/
|
|
|
|
import { getPayload } from 'payload'
|
|
import configPromise from '@payload-config'
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { logLoginFailed, logRateLimit } from '@/lib/audit/audit-service'
|
|
import {
|
|
authLimiter,
|
|
rateLimitHeaders,
|
|
getClientIpFromRequest,
|
|
isIpBlocked,
|
|
} from '@/lib/security'
|
|
|
|
/**
|
|
* 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 {
|
|
// IP-Blocklist prüfen
|
|
const clientIp = getClientIpFromRequest(req)
|
|
if (isIpBlocked(clientIp)) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Access denied' },
|
|
{ status: 403 },
|
|
)
|
|
}
|
|
|
|
// Rate-Limiting prüfen (Anti-Brute-Force)
|
|
const rateLimit = await authLimiter.check(clientIp)
|
|
if (!rateLimit.allowed) {
|
|
const payload = await getPayload({ config: configPromise })
|
|
await logRateLimit(payload, '/api/auth/login', undefined, undefined)
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Too many login attempts. Please try again later.',
|
|
},
|
|
{
|
|
status: 429,
|
|
headers: rateLimitHeaders(rateLimit, 5),
|
|
},
|
|
)
|
|
}
|
|
|
|
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 extrahieren
|
|
const clientInfo = getClientInfo(req)
|
|
|
|
// Audit-Log für fehlgeschlagenen Login mit vollem Client-Context
|
|
await logLoginFailed(payload, email, reason, clientInfo)
|
|
|
|
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.',
|
|
})
|
|
}
|