Files
2026-10-12 14:25:22 +02:00

60 lines
2.0 KiB
TypeScript

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<User> => {
const user = await User.verifyCredentials(username, password).catch(() => null)
if (!user || !(await totpService.verify(user, code))) throw new InvalidLogin()
return user
})
let result: Awaited<ReturnType<typeof penalize>>
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 }
}
}