import User from '#models/user' import limiter from '@adonisjs/limiter/services/main' import type { HttpContext } from '@adonisjs/core/http' import totpService from '#services/totp_service' import { audit } from '#services/audit' import { loginValidator } from '#validators/auth' class InvalidLogin extends Error {} /** * 5 failed attempts per IP, then blocked for 15 minutes. */ const loginLimiter = () => limiter.use({ requests: 5, duration: '15 mins', blockDuration: '15 mins' }) export default class AuthController { async login(ctx: HttpContext) { const { request, response, auth } = ctx const { username, password, code } = await request.validateUsing(loginValidator) const penalize = () => loginLimiter().penalize(`login_${request.ip()}`, async (): Promise => { const user = await User.verifyCredentials(username, password).catch(() => null) if (!user || !(await totpService.verify(user, code))) throw new InvalidLogin() return user }) let result: Awaited> try { result = await penalize() } catch (error) { if (!(error instanceof InvalidLogin)) throw error await audit(ctx, { action: 'Connexion refusée', target: username, status: 'error' }) return response.badRequest({ message: 'Identifiant, mot de passe ou code incorrect.' }) } const [throttled, user] = result if (throttled) { const minutes = Math.max(1, Math.ceil(throttled.response.availableIn / 60)) return response.tooManyRequests({ message: `Trop de tentatives. Réessaie dans ${minutes} min.`, }) } await auth.use('web').login(user!) await audit(ctx, { action: 'Connexion', username: user!.username, status: 'success' }) return { username: user!.username } } async logout(ctx: HttpContext) { await audit(ctx, { action: 'Déconnexion', status: 'success' }) await ctx.auth.use('web').logout() return { ok: true } } async me({ auth }: HttpContext) { return { username: auth.getUserOrFail().username } } }