Files
harheimertc/server/api/auth/login.post.js
Torsten Schulz (local) 58fd7fa5c6
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 5m7s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
feat(auth): implement Android refresh token handling and session management
- Added support for generating Android access tokens and managing refresh sessions in the auth endpoints.
- Implemented new tests for login, logout, and refresh functionalities specific to Android clients.
- Enhanced password reset logging with normalization and masking of email addresses.
- Created a new diagnostics endpoint for password reset attempts, including filtering and summarizing logs.
- Introduced a new utility for managing password reset logs with retention policies.
- Added tests for password reset log utilities to ensure proper functionality and privacy compliance.
- Updated WebAuthn configuration tests to validate origin handling for production and allowed origins.
2026-05-27 19:34:53 +02:00

126 lines
4.3 KiB
JavaScript

import { readUsers, writeUsers, verifyPassword, generateToken, generateAndroidAccessToken, createSession, createRefreshSession, 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
const isAndroidClient = body.client === 'android'
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] })
let token
let refreshSession = null
if (isAndroidClient) {
refreshSession = await createRefreshSession(user.id, body.deviceName)
token = generateAndroidAccessToken(user, refreshSession.session.id)
} else {
token = generateToken(user)
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)
if (isAndroidClient) {
deleteCookie(event, 'auth_token')
} else {
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
accessToken: isAndroidClient ? token : undefined,
refreshToken: refreshSession?.refreshToken,
sessionId: refreshSession?.session.id,
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
}
})