tethys.backend/app/Controllers/Http/Auth/AuthController.ts
Arno Kaimbacher 7e2f320b4f
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 44s
feat: implement activity logging for user actions and create activities table
2026-06-24 15:03:17 +02:00

138 lines
5.2 KiB
TypeScript

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`));
}
}