import type { HttpContext } from '@adonisjs/core/http'; import User from '#models/user'; import BackupCode from '#models/backup_code'; import { authValidator } from '#validators/auth'; import hash from '@adonisjs/core/services/hash'; import db from '@adonisjs/lucid/services/db'; import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider'; import ActivityLogger from '#services/activity_logger'; import logger from '@adonisjs/core/services/logger'; export default class AuthController { public async login({ request, response, auth, session }: HttpContext) { await request.validateUsing(authValidator); const { email, password } = request.only(['email', 'password']); try { await db.connection().rawQuery('SELECT 1'); const user = await User.verifyCredentials(email, password); if (user.isTwoFactorEnabled) { // Noch KEIN abgeschlossenes Login -> nicht loggen. session.flash('user_id', user.id); return response.redirect().back(); } await auth.use('web').login(user); this.recordAuthEvent('auth.login', { user, ip: request.ip() }); } catch (error: any) { // DB nicht erreichbar -> kein fehlgeschlagener Login-Versuch, weiterwerfen if (error.code === 'ECONNREFUSED') { throw error; } // Echter Credential-Fehler -> als fehlgeschlagenen Versuch protokollieren this.recordAuthEvent('auth.login_failed', { email, ip: request.ip() }); session.flash('message', 'Your username, email, or password is incorrect'); return response.redirect().back(); } return response.redirect('/apps/dashboard'); } public async twoFactorChallenge({ request, session, auth, response }: HttpContext) { const { code, backup_code, login_id } = request.only(['code', 'backup_code', 'login_id']); const user = await User.query().where('id', login_id).firstOrFail(); if (code) { const isValid = await TwoFactorAuthProvider.validate(user, code); if (isValid) { await auth.use('web').login(user); this.recordAuthEvent('auth.login', { user, email: user.email, ip: request.ip(), method: '2fa_totp' }); return response.redirect('/apps/dashboard'); } session.flash('message', 'Your two-factor code is incorrect'); return response.redirect().back(); } if (backup_code) { const codes: BackupCode[] = await user.getBackupCodes(); let backupCodeToDelete: BackupCode | null = null; for (const backupCode of codes) { const isVerified = await hash.verify(backupCode.code, backup_code); if (isVerified) { backupCodeToDelete = backupCode; break; } } if (!backupCodeToDelete) { session.flash('message', 'BackupCode not found'); return response.redirect().back(); } if (backupCodeToDelete.used) { session.flash('message', 'BackupCode already used'); return response.redirect().back(); } backupCodeToDelete.used = true; await backupCodeToDelete.save(); await auth.use('web').login(user); this.recordAuthEvent('auth.login', { user, email: user.email, ip: request.ip(), method: '2fa_backup_code' }); return response.redirect('/apps/dashboard'); } // Weder code noch backup_code übergeben session.flash('message', 'No verification code provided'); return response.redirect().back(); } public async logout({ auth, request, response }: HttpContext) { // Session auflösen -> füllt auth.user, falls eingeloggt await auth.use('web').check(); const user = auth.use('web').user; await auth.use('web').logout(); if (user) { this.recordAuthEvent('auth.logout', { user, email: user.email, ip: request.ip() }); } return response.redirect('/app/login'); } /** * Zentraler Audit-Logger für Auth-Events. * Fire-and-forget: ein Fehler beim Schreiben darf Login/Logout nie blockieren. */ private recordAuthEvent( type: 'auth.login' | 'auth.logout' | 'auth.login_failed', opts: { user?: User; email?: string; ip: string; method?: string }, ) { const { user, email, ip, method } = opts; const description = type === 'auth.login' ? `${user!.firstName} ${user!.lastName} signed in` : type === 'auth.logout' ? `${user!.firstName} ${user!.lastName} signed out` : `Failed login attempt for ${email ?? 'unknown'}`; void ActivityLogger.log({ type, description, userId: user?.id ?? null, subjectType: user ? 'User' : null, subjectId: user?.id ?? null, properties: { ip, ...(method ? { method } : {}) }, }).catch((err) => logger.error({ err }, `failed to record ${type} activity`)); } }