import type { HttpContext } from '@adonisjs/core/http' import { credentialsValidator, registerValidator, requestLoginValidator, verifyCodeValidator, } from '#validators/auth' import mail from '@adonisjs/mail/services/main' import { AuthService } from '#services/auth_service' import { inject } from '@adonisjs/core' import app from '@adonisjs/core/services/app' import env from '#start/env' import User from '#models/user' import { DateTime } from 'luxon' import redis from '@adonisjs/redis/services/main' @inject() export default class AuthController { constructor(private authService: AuthService) {} // POST /auth/request async requestLogin({ request, response, captcha }: HttpContext) { // Validate captcha if (app.inProduction) { const validateResult = await (captcha.use('turnstile') as any).validate() if (!validateResult.success) { return response.badRequest({ success: false, message: 'Veuillez valider le captcha', error: validateResult.errorCodes, }) } } const { email } = await request.validateUsing(requestLoginValidator) // Generate token const expiresIn = '15 minutes' const payload = await this.authService.generateCode(email, expiresIn) await mail .send((message) => { message .from(env.get('MAIL_FROM')!) .to(email) .subject(payload.emailTitle) .htmlView('mails/auth', payload) .textView('mails/auth-fallback', payload) }) // TODO: Handle email sending errors .then(console.log) .catch(console.error) return { success: true, } } // POST /auth/verify async verifyCode({ request, response, auth }: HttpContext) { // Validate code const { code } = await request.validateUsing(verifyCodeValidator) const { success, email } = await this.authService.validateCode(code) if (!success) { return response.badRequest({ success: false, message: 'Code de vérification invalide', }) } const user = await User.findBy('email', email) if (!user) { // If the user does not exist, return a token for registration const expiresIn = '1 hour' const { token, email: userEmail } = this.authService.generateToken(email, expiresIn) return { token, email: userEmail, success: true, } } await auth.use('web').login(user, true) // true for remember me return { success: true, } } // GET /auth/autocomplete async listNames({ request }: HttpContext) { const { className } = request.qs() if (!className) { return { success: false, message: 'Veuillez spécifier une classe', } } const lastYear = DateTime.now().minus({ years: 1 }) return User.query() .select('firstName', 'lastName', 'id') .where('className', className) .whereExists((query) => { query .from('colles') .whereRaw('colles.student_id = users.id') .where('colles.date', '>=', lastYear.toISODate()) }) .orderBy('lastName', 'asc') .then((users) => { return { success: true, data: users.map((user) => ({ value: `${user.firstName}::${user.lastName}`, label: user.fullName, userId: user.id, })), } }) } // POST /auth/register async register({ request, response, auth }: HttpContext) { const { userId, className, token } = await request.validateUsing(registerValidator) // Validate token const { success, email } = this.authService.validateToken(token) if (!success || !email) { return response.badRequest({ success: false, message: 'Votre lien de connexion est invalide ou a expiré.', }) } const user = await User.query().where('id', userId).where('className', className).first() if (!user) { return response.badRequest({ success: false, message: 'Utilisateur non trouvé. Veuillez vérifier vos informations.', }) } if (user.email && user.email.toLowerCase() !== email.toLowerCase()) { return response.badRequest({ success: false, message: "L'email associé à votre compte ne correspond pas à celui utilisé pour la connexion.", }) } user.email = email.toLowerCase() // Update email if necessary await user.save() // Perform login await auth.use('web').login(user, true) // true for remember me return { success: true, } } // POST /auth/logout async logout({ auth }: HttpContext) { await auth.use('web').logout() return { success: true, } } // POST /auth/test public async testCredentials({ request, auth }: HttpContext) { const { username, password } = await request.validateUsing(credentialsValidator) await redis.publish( 'jobs_queue', JSON.stringify({ type: 5, // Test credentials bj_username: username, bj_password: password, user_id: auth.user!.id, class_name: env.get('DEFAULT_CLASS_NAME'), }) ) return { success: true, message: 'Testing credentials...' } } // GET /auth/status public async status({ auth }: HttpContext) { const redisKey = `auth_success_${auth.user!.id}` return { success: true, data: { authenticated: await redis.get(redisKey) } } } // TODO: Magic link login // magicLink({ }: HttpContext) { // // Validate signed url (adonis) // // + login current device // // + SSE to notify other devices (and login) // } // listen({ }: HttpContext) { // // Listen for SSE events // // Need an AUTH token to connect // // AUTH token sent to client in requestLogin // } }