116 lines
3.8 KiB
JavaScript
116 lines
3.8 KiB
JavaScript
import { readUsers, writeUsers, verifyPassword, generateToken, createSession, migrateUserRoles } from '../../utils/auth.js'
|
|
import { assertRateLimit, getClientIp, registerRateLimitFailure, registerRateLimitSuccess } from '../../utils/rate-limit.js'
|
|
import { getAuthCookieOptions } from '../../utils/cookies.js'
|
|
import { writeAuditLog } from '../../utils/audit-log.js'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
try {
|
|
const body = await readBody(event)
|
|
const { email, password } = body
|
|
|
|
if (!email || !password) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
message: 'E-Mail und Passwort sind erforderlich'
|
|
})
|
|
}
|
|
|
|
const ip = getClientIp(event)
|
|
const emailKey = String(email || '').trim().toLowerCase()
|
|
|
|
// Rate Limiting (IP + Account)
|
|
assertRateLimit(event, {
|
|
name: 'auth:login:ip',
|
|
keyParts: [ip],
|
|
windowMs: 10 * 60 * 1000,
|
|
maxAttempts: 30,
|
|
lockoutMs: 15 * 60 * 1000
|
|
})
|
|
assertRateLimit(event, {
|
|
name: 'auth:login:account',
|
|
keyParts: [emailKey],
|
|
windowMs: 10 * 60 * 1000,
|
|
maxAttempts: 10,
|
|
lockoutMs: 30 * 60 * 1000
|
|
})
|
|
|
|
// Find user
|
|
const users = await readUsers()
|
|
const user = users.find(u => u.email.toLowerCase() === email.toLowerCase())
|
|
|
|
if (!user) {
|
|
await registerRateLimitFailure(event, { name: 'auth:login:ip', keyParts: [ip] })
|
|
await registerRateLimitFailure(event, { name: 'auth:login:account', keyParts: [emailKey] })
|
|
await writeAuditLog('auth.login.failed', { ip, email: emailKey, reason: 'user_not_found' })
|
|
throw createError({
|
|
statusCode: 401,
|
|
message: 'Ungültige Anmeldedaten'
|
|
})
|
|
}
|
|
|
|
// Check if user is active
|
|
if (user.active === false) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: 'Ihr Konto wurde noch nicht freigeschaltet. Bitte warten Sie auf die Bestätigung des Vorstands.'
|
|
})
|
|
}
|
|
|
|
// Verify password
|
|
const isValid = await verifyPassword(password, user.password)
|
|
if (!isValid) {
|
|
await registerRateLimitFailure(event, { name: 'auth:login:ip', keyParts: [ip] })
|
|
await registerRateLimitFailure(event, { name: 'auth:login:account', keyParts: [emailKey] })
|
|
await writeAuditLog('auth.login.failed', { ip, email: emailKey, userId: user.id, reason: 'bad_password' })
|
|
throw createError({
|
|
statusCode: 401,
|
|
message: 'Ungültige Anmeldedaten'
|
|
})
|
|
}
|
|
|
|
// Erfolg: Limiter zurücksetzen
|
|
registerRateLimitSuccess(event, { name: 'auth:login:ip', keyParts: [ip] })
|
|
registerRateLimitSuccess(event, { name: 'auth:login:account', keyParts: [emailKey] })
|
|
|
|
// Generate token
|
|
const token = generateToken(user)
|
|
|
|
// Create session
|
|
await createSession(user.id, token)
|
|
|
|
await writeAuditLog('auth.login.success', { ip, email: emailKey, userId: user.id })
|
|
|
|
// Update last login
|
|
user.lastLogin = new Date().toISOString()
|
|
const updatedUsers = users.map(u => u.id === user.id ? user : u)
|
|
await writeUsers(updatedUsers)
|
|
|
|
// Set cookie
|
|
setCookie(event, 'auth_token', token, {
|
|
...getAuthCookieOptions()
|
|
})
|
|
|
|
// Migriere Rollen falls nötig
|
|
const migratedUser = migrateUserRoles({ ...user })
|
|
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
|
|
|
// Return user data (without password) and token for API usage
|
|
return {
|
|
success: true,
|
|
token: token, // Token auch im Body für externe API-Clients
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
roles: roles
|
|
},
|
|
// Rückwärtskompatibilität: erste Rolle als role
|
|
role: roles[0] || 'mitglied'
|
|
}
|
|
} catch (error) {
|
|
console.error('Login-Fehler:', error)
|
|
throw error
|
|
}
|
|
})
|
|
|