Compare commits
	
		
			No commits in common. "c724dac0860b7ccccc5082c04ba81bfd0a6bd8a8" and "73db24d3c08e734ba5977a5ba2ef0b6622f47d77" have entirely different histories.
		
	
	
		
			c724dac086
			...
			73db24d3c0
		
	
		
					 28 changed files with 145 additions and 6004 deletions
				
			
		|  | @ -1,5 +1,5 @@ | ||||||
| import type { HttpContext } from '@adonisjs/core/http' | import type { HttpContext } from '@adonisjs/core/http' | ||||||
| import { registerValidator, requestLoginValidator, verifyCodeValidator } from '#validators/auth' | import { requestLoginValidator, verifyCodeValidator } from '#validators/auth' | ||||||
| import mail from '@adonisjs/mail/services/main' | import mail from '@adonisjs/mail/services/main' | ||||||
| import { AuthService } from '#services/auth_service' | import { AuthService } from '#services/auth_service' | ||||||
| import { inject } from '@adonisjs/core' | import { inject } from '@adonisjs/core' | ||||||
|  | @ -7,6 +7,7 @@ import app from '@adonisjs/core/services/app' | ||||||
| import env from '#start/env' | import env from '#start/env' | ||||||
| import User from '#models/user' | import User from '#models/user' | ||||||
| 
 | 
 | ||||||
|  | // TODO: When login, set user.email to the email used to request login (lowercase)
 | ||||||
| @inject() | @inject() | ||||||
| export default class AuthController { | export default class AuthController { | ||||||
|   constructor(private authService: AuthService) {} |   constructor(private authService: AuthService) {} | ||||||
|  | @ -18,18 +19,25 @@ export default class AuthController { | ||||||
|       const validateResult = await (captcha.use('turnstile') as any).validate() |       const validateResult = await (captcha.use('turnstile') as any).validate() | ||||||
|       if (!validateResult.success) { |       if (!validateResult.success) { | ||||||
|         return response.badRequest({ |         return response.badRequest({ | ||||||
|           success: false, |           message: 'Captcha validation failed', | ||||||
|           message: 'Veuillez valider le captcha', |  | ||||||
|           error: validateResult.errorCodes, |           error: validateResult.errorCodes, | ||||||
|         }) |         }) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Find user by email
 | ||||||
|     const { email } = await request.validateUsing(requestLoginValidator) |     const { email } = await request.validateUsing(requestLoginValidator) | ||||||
|  |     const user = await this.authService.findUser(email) | ||||||
|  |     if (!user) { | ||||||
|  |       return response.notFound({ | ||||||
|  |         success: false, | ||||||
|  |         message: 'Utilisateur non trouvé', | ||||||
|  |       }) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // Generate token
 |     // Generate token
 | ||||||
|     const expiresIn = '15 minutes' |     const expiresIn = '15 minutes' | ||||||
|     const payload = await this.authService.generateCode(email, expiresIn) |     const payload = await this.authService.generateToken(email, user.id, expiresIn) | ||||||
| 
 | 
 | ||||||
|     // Send email
 |     // Send email
 | ||||||
|     await mail |     await mail | ||||||
|  | @ -47,6 +55,9 @@ export default class AuthController { | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       success: true, |       success: true, | ||||||
|  |       data: { | ||||||
|  |         token: payload.token, | ||||||
|  |       }, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -54,8 +65,8 @@ export default class AuthController { | ||||||
|   async verifyCode({ request, response, auth }: HttpContext) { |   async verifyCode({ request, response, auth }: HttpContext) { | ||||||
|     // Validate code
 |     // Validate code
 | ||||||
|     const { code } = await request.validateUsing(verifyCodeValidator) |     const { code } = await request.validateUsing(verifyCodeValidator) | ||||||
|     const { success, email } = await this.authService.validateCode(code) |     const { success, userId, email } = await this.authService.validateCode(code) | ||||||
|     if (!success) { |     if (!success || !userId || isNaN(userId)) { | ||||||
|       return response.badRequest({ |       return response.badRequest({ | ||||||
|         success: false, |         success: false, | ||||||
|         message: 'Code de vérification invalide', |         message: 'Code de vérification invalide', | ||||||
|  | @ -63,78 +74,32 @@ export default class AuthController { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Find user by id
 |     // Find user by id
 | ||||||
|     const user = await User.findBy('email', email) |     const user = await User.findBy('id', userId) | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       // If the user does not exist, return a token for registration
 |       return response.notFound({ | ||||||
|       const expiresIn = '1 hour' |  | ||||||
|       const { token, email: userEmail } = this.authService.generateToken(email, expiresIn) |  | ||||||
|       return { |  | ||||||
|         token, |  | ||||||
|         email: userEmail, |  | ||||||
|         success: true, |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Perform login
 |  | ||||||
|     await auth.use('web').login(user, true) // true for remember me
 |  | ||||||
|     return { |  | ||||||
|       success: true, |  | ||||||
|       user |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // POST /auth/register
 |  | ||||||
|   async register({ request, response, auth }: HttpContext) { |  | ||||||
|     const { firstName, lastName, className, token } = await request.validateUsing(registerValidator) |  | ||||||
| 
 |  | ||||||
|     // Validate token
 |  | ||||||
|     const { success, email } = this.authService.validateToken(token) |  | ||||||
|     if (!success || !email) { |  | ||||||
|       return response.badRequest({ |  | ||||||
|         success: false, |         success: false, | ||||||
|         message: 'Votre lien de connexion est invalide ou a expiré.', |         message: 'Utilisateur non trouvé', | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Check if user already exists
 |     // Set user email to the email used to request login (lowercase)
 | ||||||
|     const existingUser = await User.findBy('email', email) |     user.email = email.toLowerCase() | ||||||
|     if (existingUser) { |     await user.save() | ||||||
|       // If user already exists, perform login
 |  | ||||||
|       await auth.use('web').login(existingUser, true) // true for remember me
 |  | ||||||
|       return { |  | ||||||
|         success: true, |  | ||||||
|         user: existingUser, |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     // TODO: Check if className is allowed (else redirect for account giving)
 |  | ||||||
| 
 |  | ||||||
|     // TODO: Rewrite user creation (NEVER CREATE USER - use string similarity)
 |  | ||||||
|     // Create new user
 |  | ||||||
|     const user = await User.create({ |  | ||||||
|       firstName, |  | ||||||
|       lastName, |  | ||||||
|       className, |  | ||||||
|       email |  | ||||||
|     }) |  | ||||||
|     // Perform login
 |     // Perform login
 | ||||||
|     await auth.use('web').login(user, true) // true for remember me
 |     await auth.use('web').login(user, true) // true for remember me
 | ||||||
|     return { |     return user | ||||||
|       success: true, |  | ||||||
|       user |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // TODO: Magic link login
 |   magicLink({}: HttpContext) { | ||||||
|   // magicLink({ }: HttpContext) {
 |     // Validate signed url (adonis)
 | ||||||
|   //   // Validate signed url (adonis)
 |     // + login current device
 | ||||||
|   //   // + login current device
 |     // + SSE to notify other devices (and login)
 | ||||||
|   //   // + SSE to notify other devices (and login)
 |   } | ||||||
|   // }
 | 
 | ||||||
| 
 |   listen({}: HttpContext) { | ||||||
|   // listen({ }: HttpContext) {
 |     // Listen for SSE events
 | ||||||
|   //   // Listen for SSE events
 |     // Need an AUTH token to connect
 | ||||||
|   //   // Need an AUTH token to connect
 |     // AUTH token sent to client in requestLogin
 | ||||||
|   //   // AUTH token sent to client in requestLogin
 |   } | ||||||
|   // }
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,168 +0,0 @@ | ||||||
| import Colle from '#models/colle' |  | ||||||
| import { ColleService } from '#services/colle_service' |  | ||||||
| import { createColleValidator, createUpcomingCollesValidator } from '#validators/colle' |  | ||||||
| import { inject } from '@adonisjs/core' |  | ||||||
| import type { HttpContext } from '@adonisjs/core/http' |  | ||||||
| import { DateTime } from 'luxon' |  | ||||||
| 
 |  | ||||||
| @inject() |  | ||||||
| export default class CollesController { |  | ||||||
|   constructor(private service: ColleService) {} |  | ||||||
| 
 |  | ||||||
|   async index({ request, response, auth }: HttpContext) { |  | ||||||
|     const { startDate: rawStartDate } = request.qs() |  | ||||||
| 
 |  | ||||||
|     // Validate startDate
 |  | ||||||
|     const startDate = rawStartDate ? DateTime.fromISO(rawStartDate, { zone: 'local' }) : null |  | ||||||
|     if (!rawStartDate || !startDate || !startDate.isValid) { |  | ||||||
|       return response.badRequest({ message: 'Invalid start date format' }) |  | ||||||
|     } |  | ||||||
|     const endDate = startDate.plus({ days: 6 }) // Sunday
 |  | ||||||
| 
 |  | ||||||
|     // Retrieve colles for the authenticated user
 |  | ||||||
|     const data = await this.service.getColles(auth.user!, startDate.toISO(), endDate.toISO()) |  | ||||||
|     return { |  | ||||||
|       success: true, |  | ||||||
|       data, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async show({ params, response, auth }: HttpContext) { |  | ||||||
|     const colleId = parseInt(params.colleId) |  | ||||||
|     if (isNaN(colleId)) { |  | ||||||
|       return response.badRequest({ message: 'Invalid colle ID' }) |  | ||||||
|     } |  | ||||||
|     // Retrieve the colle by ID
 |  | ||||||
|     const colle = await Colle.query() |  | ||||||
|       .where('id', colleId) |  | ||||||
|       .whereHas('student', (query) => { |  | ||||||
|         query.where('className', auth.user!.className) |  | ||||||
|       }) |  | ||||||
|       .preload('student') |  | ||||||
|       .preload('examiner') |  | ||||||
|       .preload('subject') |  | ||||||
|       .preload('room') |  | ||||||
|       .first() |  | ||||||
|     // TODO: Include BJID and BJSecret !
 |  | ||||||
|     if (!colle) { |  | ||||||
|       return response.notFound({ message: 'Colle not found' }) |  | ||||||
|     } |  | ||||||
|     return { |  | ||||||
|       success: true, |  | ||||||
|       data: colle, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async create({ request, response }: HttpContext) { |  | ||||||
|     const { colle: payload, className } = await request.validateUsing(createColleValidator) |  | ||||||
| 
 |  | ||||||
|     // TODO: Use Redis cache to avoid multiple queries
 |  | ||||||
|     // Retrieve or create the necessary entities (relations)
 |  | ||||||
|     const student = await this.service.getStudent(payload.student, className) |  | ||||||
|     const examiner = await this.service.getExaminer(payload.examiner) |  | ||||||
|     const subject = await this.service.getSubject(payload.subject) |  | ||||||
|     const room = await this.service.getRoom(payload.room) |  | ||||||
|     const date = DateTime.fromJSDate(payload.date) |  | ||||||
|     if (!date.isValid) { |  | ||||||
|       return response.badRequest({ message: 'Invalid date format' }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const colleData = { |  | ||||||
|       studentId: student.id, |  | ||||||
|       examinerId: examiner.id, |  | ||||||
|       subjectId: subject.id, |  | ||||||
|       roomId: room.id, |  | ||||||
|       bjsecret: payload.bjsecret, |  | ||||||
|       bjid: payload.bjid, |  | ||||||
|       grade: payload.grade, |  | ||||||
|       content: payload.content, |  | ||||||
|       comment: payload.comment, |  | ||||||
|       date, |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Check if the colle already exists
 |  | ||||||
|     const colle = await Colle.query() |  | ||||||
|       .where('studentId', student.id) |  | ||||||
|       .where('subjectId', subject.id) |  | ||||||
|       .where('date', date.toISO()) |  | ||||||
|       .first() |  | ||||||
| 
 |  | ||||||
|     // If it exists, update the existing colle
 |  | ||||||
|     if (colle) { |  | ||||||
|       Object.assign(colle, colleData) |  | ||||||
|       return colle.save() |  | ||||||
|     } |  | ||||||
|     // Create the colle
 |  | ||||||
|     return Colle.create(colleData) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async createUpcoming({ request }: HttpContext) { |  | ||||||
|     const payload = await request.validateUsing(createUpcomingCollesValidator) |  | ||||||
|     // ONLY UPCOMING COLLES
 |  | ||||||
|     const now = DateTime.now() |  | ||||||
|     const colles = payload.colles.filter( |  | ||||||
|       (colle) => colle.date && DateTime.fromJSDate(colle.date) > now |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     // Retrieve all upcoming colles for the class
 |  | ||||||
|     const upcomingColles = await Colle.query() |  | ||||||
|       .whereHas('student', (query) => { |  | ||||||
|         query.where('className', payload.className) |  | ||||||
|       }) |  | ||||||
|       .where('date', '>=', now.toISO()) |  | ||||||
|       .orderBy('date', 'asc') |  | ||||||
|     // Store the upcoming colles ids
 |  | ||||||
|     const upcomingCollesIds = new Set(upcomingColles.map((colle) => colle.id)) |  | ||||||
| 
 |  | ||||||
|     for await (const colle of colles) { |  | ||||||
|       // Find the updated data for the colle
 |  | ||||||
|       const student = await this.service.getStudent(colle.student, payload.className) |  | ||||||
|       const examiner = await this.service.getExaminer(colle.examiner) |  | ||||||
|       const subject = await this.service.getSubject(colle.subject) |  | ||||||
|       const room = await this.service.getRoom(colle.room) |  | ||||||
|       const date = DateTime.fromJSDate(colle.date) |  | ||||||
| 
 |  | ||||||
|       const oldColle = upcomingColles.find( |  | ||||||
|         (c) => |  | ||||||
|           c.studentId === student.id && |  | ||||||
|           c.subjectId === subject.id && |  | ||||||
|           c.date.toISO() === date.toISO() |  | ||||||
|       ) |  | ||||||
| 
 |  | ||||||
|       const updatedColle = { |  | ||||||
|         studentId: student.id, |  | ||||||
|         examinerId: examiner.id, |  | ||||||
|         subjectId: subject.id, |  | ||||||
|         roomId: room.id, |  | ||||||
|         bjsecret: colle.bjsecret, |  | ||||||
|         bjid: colle.bjid, |  | ||||||
|         grade: colle.grade, |  | ||||||
|         content: colle.content, |  | ||||||
|         comment: colle.comment, |  | ||||||
|         date, |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // Create a new colle if it doesn't exist
 |  | ||||||
|       if (!oldColle) { |  | ||||||
|         await Colle.create(updatedColle) |  | ||||||
|         continue |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // Update the colle with the new data
 |  | ||||||
|       // and remove it from the list
 |  | ||||||
|       Object.assign(oldColle, updatedColle) |  | ||||||
|       await oldColle.save() |  | ||||||
|       upcomingCollesIds.delete(oldColle.id) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Delete the colles that were not updated
 |  | ||||||
|     const deleted = await Colle.query() |  | ||||||
|       .whereHas('student', (query) => { |  | ||||||
|         query.where('className', payload.className) |  | ||||||
|       }) |  | ||||||
|       .whereIn('id', Array.from(upcomingCollesIds)) |  | ||||||
|       .delete() |  | ||||||
| 
 |  | ||||||
|     console.log(`Deleted ${deleted} upcoming colles that were not updated`) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -21,8 +21,7 @@ export default class UserController { | ||||||
|     // const avatar = await drive.use().getSignedUrl(key)
 |     // const avatar = await drive.use().getSignedUrl(key)
 | ||||||
|     return User.create({ |     return User.create({ | ||||||
|       ...payload, |       ...payload, | ||||||
|       // TODO: No avatar for now!!
 |       avatar, | ||||||
|       // avatar,
 |  | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ export default class HttpExceptionHandler extends ExceptionHandler { | ||||||
|    */ |    */ | ||||||
|   async handle(error: any, ctx: HttpContext) { |   async handle(error: any, ctx: HttpContext) { | ||||||
|     const statusCode = error.status || error.statusCode || 500 |     const statusCode = error.status || error.statusCode || 500 | ||||||
|     const message = error.messages || error.message || 'Internal Server Error' |     const message = error.message || 'Internal Server Error' | ||||||
|     const stack = this.debug ? error.stack : undefined |     const stack = this.debug ? error.stack : undefined | ||||||
|     const response = { |     const response = { | ||||||
|       status: statusCode, |       status: statusCode, | ||||||
|  |  | ||||||
|  | @ -1,72 +0,0 @@ | ||||||
| import { DateTime } from 'luxon' |  | ||||||
| import { BaseModel, belongsTo, column, hasMany } from '@adonisjs/lucid/orm' |  | ||||||
| import User from './user.js' |  | ||||||
| import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations' |  | ||||||
| import Subject from './subject.js' |  | ||||||
| import Room from './room.js' |  | ||||||
| import ColleAttachment from './colle_attachment.js' |  | ||||||
| import Examiner from './examiner.js' |  | ||||||
| 
 |  | ||||||
| export default class Colle extends BaseModel { |  | ||||||
|   @column({ isPrimary: true }) |  | ||||||
|   declare id: number |  | ||||||
| 
 |  | ||||||
|   @belongsTo(() => User, { foreignKey: 'studentId' }) |  | ||||||
|   declare student: BelongsTo<typeof User> |  | ||||||
| 
 |  | ||||||
|   @column({ serializeAs: null }) |  | ||||||
|   declare studentId: number |  | ||||||
| 
 |  | ||||||
|   @belongsTo(() => Examiner) |  | ||||||
|   declare examiner: BelongsTo<typeof Examiner> |  | ||||||
| 
 |  | ||||||
|   @column({ serializeAs: null }) |  | ||||||
|   declare examinerId: number |  | ||||||
| 
 |  | ||||||
|   // @computed()
 |  | ||||||
|   // get examinerName(): string {
 |  | ||||||
|   //   return this.examiner?.name
 |  | ||||||
|   // }
 |  | ||||||
| 
 |  | ||||||
|   // Bjcolle data
 |  | ||||||
|   @column({ serializeAs: null }) |  | ||||||
|   declare bjsecret: string |  | ||||||
| 
 |  | ||||||
|   @column({ serializeAs: null }) |  | ||||||
|   declare bjid: string |  | ||||||
| 
 |  | ||||||
|   // Colle data
 |  | ||||||
|   @belongsTo(() => Subject) |  | ||||||
|   declare subject: BelongsTo<typeof Subject> |  | ||||||
| 
 |  | ||||||
|   @column({ serializeAs: null }) |  | ||||||
|   declare subjectId: number |  | ||||||
| 
 |  | ||||||
|   @belongsTo(() => Room) |  | ||||||
|   declare room: BelongsTo<typeof Room> |  | ||||||
| 
 |  | ||||||
|   @column({ serializeAs: null }) |  | ||||||
|   declare roomId: number |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare grade: number |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare content: string |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare comment: string |  | ||||||
| 
 |  | ||||||
|   @hasMany(() => ColleAttachment) |  | ||||||
|   declare attachments: HasMany<typeof ColleAttachment> |  | ||||||
| 
 |  | ||||||
|   // Time data
 |  | ||||||
|   @column.dateTime() |  | ||||||
|   declare date: DateTime |  | ||||||
| 
 |  | ||||||
|   @column.dateTime({ autoCreate: true }) |  | ||||||
|   declare createdAt: DateTime |  | ||||||
| 
 |  | ||||||
|   @column.dateTime({ autoCreate: true, autoUpdate: true }) |  | ||||||
|   declare updatedAt: DateTime |  | ||||||
| } |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| import { BaseModel, column } from '@adonisjs/lucid/orm' |  | ||||||
| 
 |  | ||||||
| export default class ColleAttachment extends BaseModel { |  | ||||||
|   @column({ isPrimary: true }) |  | ||||||
|   declare id: number |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare name: string |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare path: string |  | ||||||
| } |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| import { BaseModel, column } from '@adonisjs/lucid/orm' |  | ||||||
| 
 |  | ||||||
| export default class Examiner extends BaseModel { |  | ||||||
|   @column({ isPrimary: true }) |  | ||||||
|   declare id: number |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare name: string |  | ||||||
| } |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| import { BaseModel, column } from '@adonisjs/lucid/orm' |  | ||||||
| 
 |  | ||||||
| export default class Room extends BaseModel { |  | ||||||
|   @column({ isPrimary: true }) |  | ||||||
|   declare id: number |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare name: string |  | ||||||
| } |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| import { BaseModel, column } from '@adonisjs/lucid/orm' |  | ||||||
| 
 |  | ||||||
| export default class Subject extends BaseModel { |  | ||||||
|   @column({ isPrimary: true }) |  | ||||||
|   declare id: number |  | ||||||
| 
 |  | ||||||
|   @column() |  | ||||||
|   declare name: string |  | ||||||
| } |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { DateTime } from 'luxon' | import { DateTime } from 'luxon' | ||||||
| import { BaseModel, column, computed } from '@adonisjs/lucid/orm' | import { BaseModel, column } from '@adonisjs/lucid/orm' | ||||||
| import { DbRememberMeTokensProvider } from '@adonisjs/auth/session' | import { DbRememberMeTokensProvider } from '@adonisjs/auth/session' | ||||||
| 
 | 
 | ||||||
| export default class User extends BaseModel { | export default class User extends BaseModel { | ||||||
|  | @ -17,14 +17,12 @@ export default class User extends BaseModel { | ||||||
|   @column() |   @column() | ||||||
|   declare lastName: string |   declare lastName: string | ||||||
| 
 | 
 | ||||||
|   @computed() |  | ||||||
|   get fullName() { |  | ||||||
|     return `${this.firstName} ${this.lastName}` |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @column() |   @column() | ||||||
|   declare email: string |   declare email: string | ||||||
| 
 | 
 | ||||||
|  |   @column() | ||||||
|  |   declare avatar: string | ||||||
|  | 
 | ||||||
|   @column.dateTime({ autoCreate: true }) |   @column.dateTime({ autoCreate: true }) | ||||||
|   declare createdAt: DateTime |   declare createdAt: DateTime | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,17 @@ | ||||||
| import encryption from '@adonisjs/core/services/encryption' | import encryption from '@adonisjs/core/services/encryption' | ||||||
| import cache from '@adonisjs/cache/services/main' | import cache from '@adonisjs/cache/services/main' | ||||||
| import env from '#start/env' | import env from '#start/env' | ||||||
|  | import User from '#models/user' | ||||||
|  | import { CmpStrAsync } from 'cmpstr' | ||||||
|  | 
 | ||||||
|  | const cmp = CmpStrAsync.create().setMetric('levenshtein').setFlags('i') | ||||||
| 
 | 
 | ||||||
| export class AuthService { | export class AuthService { | ||||||
|   async generateCode(email: string, expiresIn: string) { |   async generateToken(email: string, userId: number, expiresIn: string) { | ||||||
|     // TODO: Generate magic link token
 |     // Generate magic link token
 | ||||||
|     // const identifier = email
 |     const identifier = `${email}:${userId}` | ||||||
|     // const token = encryption.encrypt(identifier, expiresIn)
 |     const token = encryption.encrypt(identifier, expiresIn) | ||||||
|     // const magicLink = env.get('PUBLIC_AUTH_URL') + '/success?signature=' + token
 |     const magicLink = env.get('PUBLIC_AUTH_URL') + '/success?signature=' + token | ||||||
| 
 | 
 | ||||||
|     // Generate code
 |     // Generate code
 | ||||||
|     const formattedOTP = Math.floor(Math.random() * 1000000) |     const formattedOTP = Math.floor(Math.random() * 1000000) | ||||||
|  | @ -15,7 +19,7 @@ export class AuthService { | ||||||
|       .padStart(6, '0') |       .padStart(6, '0') | ||||||
|     await cache.set({ |     await cache.set({ | ||||||
|       key: 'auth:otp:' + formattedOTP, |       key: 'auth:otp:' + formattedOTP, | ||||||
|       value: email, |       value: identifier, | ||||||
|       ttl: expiresIn, |       ttl: expiresIn, | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|  | @ -23,52 +27,81 @@ export class AuthService { | ||||||
|     return { |     return { | ||||||
|       emailTitle, |       emailTitle, | ||||||
|       formattedOTP, |       formattedOTP, | ||||||
|       // magicLink,
 |       magicLink, | ||||||
|       expiresIn, |       expiresIn, | ||||||
|       email, |       email, | ||||||
|       // token,
 |       token, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   parseIdentifier(id: string) { | ||||||
|  |     const [email, userId] = id.split(':') | ||||||
|  |     return { | ||||||
|  |       email: email.toLowerCase(), | ||||||
|  |       userId: parseInt(userId, 10), | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async validateCode(code: string) { |   async validateCode(code: string) { | ||||||
|     // Validate code
 |     // Validate code
 | ||||||
|     const key = 'auth:otp:' + code |     const key = 'auth:otp:' + code | ||||||
|     const email = await cache.get({ key }) |     const id = await cache.get({ key }) | ||||||
|     if (!email) return { success: false, email: null } |     if (!id) return { success: false, userId: null, email: null } | ||||||
| 
 | 
 | ||||||
|     // Delete code from cache
 |     // Delete code from cache
 | ||||||
|     await cache.delete({ key }) |     await cache.delete({ key }) | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       email, |  | ||||||
|       success: true, |       success: true, | ||||||
|  |       ...this.parseIdentifier(id) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   generateToken(email: string, expiresIn: string) { |   parseNameFromEmail(email: string): Promise<string> { | ||||||
|     // Generate token
 |     // Parse name from email
 | ||||||
|     const identifier = email.toLowerCase() |     return new Promise((resolve, reject) => { | ||||||
|     const token = encryption.encrypt(identifier, expiresIn) |       try { | ||||||
| 
 |         const [firstName, lastName] = email.split('@')[0].split('.') | ||||||
|     return { |         resolve(`${firstName} ${lastName}`.toLowerCase()) | ||||||
|       token, |       } catch (error) { | ||||||
|       expiresIn, |         reject(new Error('Invalid email format')) | ||||||
|       email: identifier, |  | ||||||
|       } |       } | ||||||
|  |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   validateToken(token: string) { |   findUserByEmail(email: string) { | ||||||
|     // Decrypt token
 |     return User.query().where('email', email).first() | ||||||
|     const decrypted = encryption.decrypt(token) |   } | ||||||
|     if (!decrypted) return { success: false, email: null } |  | ||||||
| 
 | 
 | ||||||
|     // Validate email format
 |   async findUser(email: string) { | ||||||
|     const email = (decrypted as string).toLowerCase() |     // Try to find user by email
 | ||||||
|     if (!email.includes('@')) return { success: false, email: null } |     let user: User | null | undefined = await this.findUserByEmail(email) | ||||||
|  |     if (user) return user | ||||||
| 
 | 
 | ||||||
|     return { |     // If not found, try to parse name from email and find user by name
 | ||||||
|       success: true, |     const name = await this.parseNameFromEmail(email) | ||||||
|       email, |     const users = await User.query() | ||||||
|     } |       // Select all users with no email
 | ||||||
|  |       .whereNull('email') | ||||||
|  |     const names = users.map((user) => `${user.firstName} ${user.lastName}`.toLowerCase()) | ||||||
|  | 
 | ||||||
|  |     // Search for similar names
 | ||||||
|  |     const data = await cmp.searchAsync(name, names) | ||||||
|  |     if (data.length === 0) { | ||||||
|  |       console.warn(`No user found for email "${email}" or name "${name}"`) | ||||||
|  |       return null | ||||||
|  |     } | ||||||
|  |     const source = data[0] | ||||||
|  |     const { match: similarity } = await cmp.testAsync(source, name) | ||||||
|  | 
 | ||||||
|  |     console.log(similarity) | ||||||
|  |     // If similarity is high enough, return the user
 | ||||||
|  |     if (similarity > 0.8) { | ||||||
|  |       user = users.find((u) => `${u.firstName} ${u.lastName}`.toLowerCase() === source) | ||||||
|  |       return user | ||||||
|  |     } | ||||||
|  |     console.warn( | ||||||
|  |       `No user found for email "${email}" or name "${name}". Similarity: ${similarity} with source "${source}"` | ||||||
|  |     ) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,95 +0,0 @@ | ||||||
| import Colle from '#models/colle' |  | ||||||
| import Examiner from '#models/examiner' |  | ||||||
| import Room from '#models/room' |  | ||||||
| import Subject from '#models/subject' |  | ||||||
| import User from '#models/user' |  | ||||||
| 
 |  | ||||||
| export class ColleService { |  | ||||||
|   async getStudent(studentName: string, className: string) { |  | ||||||
|     // Find or create a student by name
 |  | ||||||
|     const { firstName, lastName } = this.splitNames(studentName) |  | ||||||
|     const student = await User.query() |  | ||||||
|       .where('first_name', firstName) |  | ||||||
|       .where('last_name', lastName) |  | ||||||
|       .where('class_name', className) |  | ||||||
|       .first() |  | ||||||
|     if (!student) { |  | ||||||
|       return User.create({ firstName, lastName, className }) |  | ||||||
|     } |  | ||||||
|     return student |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   splitNames(fullName: string): { firstName: string; lastName: string } { |  | ||||||
|     const words = fullName.trim().split(/\s+/) |  | ||||||
| 
 |  | ||||||
|     let lastNameParts: string[] = [] |  | ||||||
|     let i = 0 |  | ||||||
| 
 |  | ||||||
|     // Collect all uppercase words at the start
 |  | ||||||
|     while (i < words.length && words[i] === words[i].toUpperCase()) { |  | ||||||
|       lastNameParts.push(words[i]) |  | ||||||
|       i++ |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const lastName = lastNameParts.join(' ') |  | ||||||
|     const firstName = words.slice(i).join(' ') |  | ||||||
| 
 |  | ||||||
|     return { firstName, lastName } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async getExaminer(examinerName: string) { |  | ||||||
|     // Find or create an examiner by name
 |  | ||||||
|     const examiner = await Examiner.query().where('name', examinerName).first() |  | ||||||
|     if (!examiner) return Examiner.create({ name: examinerName }) |  | ||||||
|     return examiner |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async getSubject(subjectName: string) { |  | ||||||
|     // Find or create a subject by name
 |  | ||||||
|     const subject = await Subject.query().where('name', subjectName).first() |  | ||||||
|     if (!subject) return Subject.create({ name: subjectName }) |  | ||||||
|     return subject |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async getRoom(roomName: string) { |  | ||||||
|     // Find or create a room by name
 |  | ||||||
|     const room = await Room.query().where('name', roomName).first() |  | ||||||
|     if (!room) return Room.create({ name: roomName }) |  | ||||||
|     return room |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async getColles(student: User, startDate: string, endDate: string) { |  | ||||||
|     const classColles = await Colle.query() |  | ||||||
|       .preload('student') |  | ||||||
|       .preload('examiner') |  | ||||||
|       .preload('subject') |  | ||||||
|       .preload('room') |  | ||||||
|       .where('date', '>=', startDate) |  | ||||||
|       .where('date', '<=', endDate) |  | ||||||
|       .whereHas('student', (query) => { |  | ||||||
|         query.where('className', student.className) |  | ||||||
|       }) |  | ||||||
|       // Filter only colles that have been graded
 |  | ||||||
|       .whereNotNull('grade') |  | ||||||
|       .orderBy('date', 'asc') |  | ||||||
| 
 |  | ||||||
|     const studentColles = await Colle.query() |  | ||||||
|       .preload('examiner') |  | ||||||
|       .preload('subject') |  | ||||||
|       .preload('room') |  | ||||||
|       .preload('student') |  | ||||||
|       .where('date', '>=', startDate) |  | ||||||
|       .where('date', '<=', endDate) |  | ||||||
|       .where('studentId', student.id) |  | ||||||
|       .orderBy('date', 'asc') |  | ||||||
| 
 |  | ||||||
|     // TODO: Favorite colles
 |  | ||||||
|     const favoriteColles = [] as Colle[] |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|       classColles, |  | ||||||
|       studentColles, |  | ||||||
|       favoriteColles, |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -9,7 +9,7 @@ export const requestLoginValidator = vine.compile( | ||||||
|       }) |       }) | ||||||
|       .normalizeEmail({ |       .normalizeEmail({ | ||||||
|         all_lowercase: true, |         all_lowercase: true, | ||||||
|       }).trim(), |       }), | ||||||
|   }) |   }) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -19,36 +19,20 @@ export const verifyCodeValidator = vine.compile( | ||||||
|   }) |   }) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| function toTitleCase(value: string) { | export const magicLinkValidator = vine.compile( | ||||||
|   return value.replace(/\w\S*/g, (txt) => { |  | ||||||
|     return txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase() |  | ||||||
|   }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export const registerValidator = vine.compile( |  | ||||||
|   vine.object({ |   vine.object({ | ||||||
|     firstName: vine.string().minLength(2).maxLength(50).trim().transform(toTitleCase), |  | ||||||
|     lastName: vine.string().minLength(2).maxLength(50).trim().toUpperCase(), |  | ||||||
|     className: vine.string().minLength(2).maxLength(50), |  | ||||||
|     token: vine.string(), |     token: vine.string(), | ||||||
|   }) |   }) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // TODO: Magic link login
 | export const listenValidator = vine.compile( | ||||||
| // export const magicLinkValidator = vine.compile(
 |   vine.object({ | ||||||
| //   vine.object({
 |     token: vine.string().uuid(), | ||||||
| //     token: vine.string(),
 |   }) | ||||||
| //   })
 | ) | ||||||
| // )
 |  | ||||||
| 
 | 
 | ||||||
| // export const listenValidator = vine.compile(
 | export const exchangeTokenValidator = vine.compile( | ||||||
| //   vine.object({
 |   vine.object({ | ||||||
| //     token: vine.string().uuid(),
 |     token: vine.string().uuid(), | ||||||
| //   })
 |   }) | ||||||
| // )
 | ) | ||||||
| 
 |  | ||||||
| // export const exchangeTokenValidator = vine.compile(
 |  | ||||||
| //   vine.object({
 |  | ||||||
| //     token: vine.string().uuid(),
 |  | ||||||
| //   })
 |  | ||||||
| // )
 |  | ||||||
|  |  | ||||||
|  | @ -1,30 +0,0 @@ | ||||||
| import vine from '@vinejs/vine' |  | ||||||
| 
 |  | ||||||
| const colle = vine.object({ |  | ||||||
|   student: vine.string(), |  | ||||||
|   examiner: vine.string(), |  | ||||||
|   subject: vine.string(), |  | ||||||
|   room: vine.string(), |  | ||||||
|   grade: vine.number().min(-1).max(20).optional(), |  | ||||||
|   content: vine.string().optional(), |  | ||||||
|   comment: vine.string().optional(), |  | ||||||
|   date: vine.date({ formats: ['iso8601'] }), |  | ||||||
|   bjsecret: vine.string().optional(), |  | ||||||
|   bjid: vine.string().optional(), |  | ||||||
|   // TODO: Add attachments validation
 |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| const className = vine.string() |  | ||||||
| 
 |  | ||||||
| export const createColleValidator = vine.compile(vine.object({ |  | ||||||
|   colle, |  | ||||||
|   className, |  | ||||||
| })) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| export const createUpcomingCollesValidator = vine.compile( |  | ||||||
|   vine.object({ |  | ||||||
|     colles: vine.array(colle), |  | ||||||
|     className, |  | ||||||
|   }), |  | ||||||
| ) |  | ||||||
|  | @ -9,7 +9,7 @@ import { defineConfig } from '@adonisjs/cors' | ||||||
| const corsConfig = defineConfig({ | const corsConfig = defineConfig({ | ||||||
|   enabled: true, |   enabled: true, | ||||||
|   // TODO: Only same domain
 |   // TODO: Only same domain
 | ||||||
|   origin: '*', |   origin: true, | ||||||
|   methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], |   methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], | ||||||
|   headers: true, |   headers: true, | ||||||
|   exposeHeaders: [], |   exposeHeaders: [], | ||||||
|  |  | ||||||
|  | @ -5,11 +5,12 @@ export default class extends BaseSchema { | ||||||
| 
 | 
 | ||||||
|   async up() { |   async up() { | ||||||
|     this.schema.createTable(this.tableName, (table) => { |     this.schema.createTable(this.tableName, (table) => { | ||||||
|       table.increments('id').primary() |       table.increments('id').notNullable() | ||||||
|       table.string('class_name', 10).notNullable() |       table.string('class_name', 10).notNullable() | ||||||
|       table.string('first_name', 50).notNullable() |       table.string('first_name', 50).notNullable() | ||||||
|       table.string('last_name', 50).notNullable() |       table.string('last_name', 50).notNullable() | ||||||
|       table.string('email', 254).unique().nullable() |       table.string('email', 254).unique().nullable() | ||||||
|  |       table.string('avatar', 254).notNullable() | ||||||
|       table.timestamp('created_at').notNullable() |       table.timestamp('created_at').notNullable() | ||||||
|       table.timestamp('updated_at').nullable() |       table.timestamp('updated_at').nullable() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| import { BaseSchema } from '@adonisjs/lucid/schema' |  | ||||||
| 
 |  | ||||||
| export default class extends BaseSchema { |  | ||||||
|   protected tableName = 'rooms' |  | ||||||
| 
 |  | ||||||
|   async up() { |  | ||||||
|     this.schema.createTable(this.tableName, (table) => { |  | ||||||
|       table.increments('id').primary() |  | ||||||
|       table.string('name').notNullable() |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async down() { |  | ||||||
|     this.schema.dropTable(this.tableName) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| import { BaseSchema } from '@adonisjs/lucid/schema' |  | ||||||
| 
 |  | ||||||
| export default class extends BaseSchema { |  | ||||||
|   protected tableName = 'subjects' |  | ||||||
| 
 |  | ||||||
|   async up() { |  | ||||||
|     this.schema.createTable(this.tableName, (table) => { |  | ||||||
|       table.increments('id').primary() |  | ||||||
|       table.string('name').notNullable() |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async down() { |  | ||||||
|     this.schema.dropTable(this.tableName) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| import { BaseSchema } from '@adonisjs/lucid/schema' |  | ||||||
| 
 |  | ||||||
| export default class extends BaseSchema { |  | ||||||
|   protected tableName = 'examiners' |  | ||||||
| 
 |  | ||||||
|   async up() { |  | ||||||
|     this.schema.createTable(this.tableName, (table) => { |  | ||||||
|       table.increments('id').primary() |  | ||||||
|       table.string('name').notNullable() |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async down() { |  | ||||||
|     this.schema.dropTable(this.tableName) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| import { BaseSchema } from '@adonisjs/lucid/schema' |  | ||||||
| 
 |  | ||||||
| export default class extends BaseSchema { |  | ||||||
|   protected tableName = 'colles' |  | ||||||
| 
 |  | ||||||
|   async up() { |  | ||||||
|     this.schema.createTable(this.tableName, (table) => { |  | ||||||
|       table.increments('id').primary(); |  | ||||||
| 
 |  | ||||||
|       // Relations
 |  | ||||||
|       table.integer('student_id').unsigned().references('id').inTable('users').onDelete('CASCADE'); |  | ||||||
|       table.integer('examiner_id').unsigned().references('id').inTable('examiners').onDelete('CASCADE'); |  | ||||||
|       table.integer('subject_id').unsigned().references('id').inTable('subjects').onDelete('SET NULL'); |  | ||||||
|       table.integer('room_id').unsigned().references('id').inTable('rooms').onDelete('SET NULL'); |  | ||||||
| 
 |  | ||||||
|       // Bjcolle data
 |  | ||||||
|       table.string('bjsecret').nullable() |  | ||||||
|       table.string('bjid').nullable() |  | ||||||
| 
 |  | ||||||
|       // Colle data
 |  | ||||||
|       table.decimal('grade', 5, 2).nullable() |  | ||||||
|       table.text('content').nullable() |  | ||||||
|       table.text('comment').nullable() |  | ||||||
| 
 |  | ||||||
|       // Time data
 |  | ||||||
|       table.dateTime('date').notNullable() |  | ||||||
|       table.timestamp('created_at').notNullable() |  | ||||||
|       table.timestamp('updated_at').notNullable() |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async down() { |  | ||||||
|     this.schema.dropTable(this.tableName) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| import { BaseSchema } from '@adonisjs/lucid/schema' |  | ||||||
| 
 |  | ||||||
| export default class extends BaseSchema { |  | ||||||
|   protected tableName = 'colle_attachments' |  | ||||||
| 
 |  | ||||||
|   async up() { |  | ||||||
|     this.schema.createTable(this.tableName, (table) => { |  | ||||||
|       table.increments('id').primary() |  | ||||||
|       table.string('name').notNullable() |  | ||||||
|       table.string('path').notNullable() |  | ||||||
|       table |  | ||||||
|         .integer('colle_id') |  | ||||||
|         .unsigned() |  | ||||||
|         .references('colles.id') |  | ||||||
|         .onDelete('CASCADE') |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async down() { |  | ||||||
|     this.schema.dropTable(this.tableName) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -18,18 +18,3 @@ services: | ||||||
|       - ./data:/var/lib/postgresql/data |       - ./data:/var/lib/postgresql/data | ||||||
|     ports: |     ports: | ||||||
|       - "127.0.0.1:5432:5432" |       - "127.0.0.1:5432:5432" | ||||||
| 
 |  | ||||||
|   pgadmin: |  | ||||||
|       image: dpage/pgadmin4 |  | ||||||
|       container_name: pgadmin4_container |  | ||||||
|       restart: unless-stopped |  | ||||||
|       ports: |  | ||||||
|         - "8888:80" |  | ||||||
|       environment: |  | ||||||
|         PGADMIN_DEFAULT_EMAIL: nathan@lamy-charrier.fr |  | ||||||
|         PGADMIN_DEFAULT_PASSWORD: securepass |  | ||||||
|       volumes: |  | ||||||
|         - pgadmin-data:/var/lib/pgadmin |  | ||||||
| 
 |  | ||||||
| volumes: |  | ||||||
|   pgadmin-data: |  | ||||||
|  | @ -63,6 +63,7 @@ | ||||||
|     "@adonisjs/transmit": "^2.0.2", |     "@adonisjs/transmit": "^2.0.2", | ||||||
|     "@vinejs/vine": "^3.0.1", |     "@vinejs/vine": "^3.0.1", | ||||||
|     "adonis-captcha-guard": "^1.0.1", |     "adonis-captcha-guard": "^1.0.1", | ||||||
|  |     "cmpstr": "^3.0.1", | ||||||
|     "edge.js": "^6.2.1", |     "edge.js": "^6.2.1", | ||||||
|     "luxon": "^3.6.1", |     "luxon": "^3.6.1", | ||||||
|     "pg": "^8.16.0", |     "pg": "^8.16.0", | ||||||
|  |  | ||||||
							
								
								
									
										5306
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										5306
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -2,15 +2,18 @@ Bonjour, | ||||||
| 
 | 
 | ||||||
| Nous avons reçu une demande de connexion à votre compte Khollisé. | Nous avons reçu une demande de connexion à votre compte Khollisé. | ||||||
| 
 | 
 | ||||||
| Utilisez ce code de vérification à 6 chiffres pour vous connecter : | Cliquez sur ce lien pour vous connecter : | ||||||
|  | {{ magicLink }} | ||||||
|  | 
 | ||||||
|  | Vous pouvez également entrer ce code de vérification à 6 chiffres : | ||||||
| {{ formattedOTP }} | {{ formattedOTP }} | ||||||
| 
 | 
 | ||||||
| Ce code expirera dans {{ expiresIn }} et ne peut être utilisé qu'une seule fois. | Ce lien et ce code expireront dans {{ expiresIn }} et ne peuvent être utilisés qu'une seule fois. | ||||||
| 
 | 
 | ||||||
| Si vous n'avez pas demandé cette connexion, vous pouvez ignorer cet email. | Si vous n'avez pas demandé cette connexion, vous pouvez ignorer cet e-mail. | ||||||
| 
 | 
 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| Cet email a été envoyé à {{ email }} | Cet e-mail a été envoyé à {{ email }} | ||||||
| 
 | 
 | ||||||
| © {{ new Date().getFullYear() }} Khollisé. Tous droits réservés. | © {{ new Date().getFullYear() }} Khollisé. Tous droits réservés. | ||||||
|  |  | ||||||
|  | @ -11,7 +11,17 @@ | ||||||
|         <p style="margin-bottom: 16px;">Bonjour,</p> |         <p style="margin-bottom: 16px;">Bonjour,</p> | ||||||
| 
 | 
 | ||||||
|         <p style="margin-bottom: 16px;"> |         <p style="margin-bottom: 16px;"> | ||||||
|           Nous avons reçu une demande de connexion à votre compte Khollisé. Utilisez le code de vérification ci-dessous pour vous connecter : |           Nous avons reçu une demande de connexion à votre compte Khollisé. Utilisez le bouton ci-dessous pour vous connecter instantanément : | ||||||
|  |         </p> | ||||||
|  | 
 | ||||||
|  |         <div style="text-align: center;"> | ||||||
|  |           <a href="{{ magicLink }}" style="display: inline-block; background-color: #4f46e5; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 4px; font-weight: 600; margin: 16px 0; text-align: center;">Se connecter</a> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <hr style="margin: 32px 0; border: none; border-top: 1px solid #e5e7eb;" /> | ||||||
|  | 
 | ||||||
|  |         <p style="margin-bottom: 16px;"> | ||||||
|  |           Vous pouvez également utiliser le code de vérification à 6 chiffres ci-dessous : | ||||||
|         </p> |         </p> | ||||||
| 
 | 
 | ||||||
|         <div style="margin: 24px 0; text-align: center;"> |         <div style="margin: 24px 0; text-align: center;"> | ||||||
|  | @ -21,7 +31,7 @@ | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <p style="font-size: 14px; color: #6b7280; font-style: italic; margin-bottom: 16px;"> |         <p style="font-size: 14px; color: #6b7280; font-style: italic; margin-bottom: 16px;"> | ||||||
|           Le code ci-dessus expirera dans {{ expiresIn }} et ne peut être utilisé qu'une seule fois. |           Le lien et le code ci-dessus expireront dans {{ expiresIn }} et ne peuvent être utilisés qu'une seule fois. | ||||||
|         </p> |         </p> | ||||||
| 
 | 
 | ||||||
|         <p style="font-size: 14px; color: #6b7280; font-style: italic; margin-bottom: 16px;"> |         <p style="font-size: 14px; color: #6b7280; font-style: italic; margin-bottom: 16px;"> | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import router from '@adonisjs/core/services/router' | ||||||
| import transmit from '@adonisjs/transmit/services/main' | import transmit from '@adonisjs/transmit/services/main' | ||||||
| import { authThrottle } from './limiters.js' | import { authThrottle } from './limiters.js' | ||||||
| import { throttle } from './limiter.js' | import { throttle } from './limiter.js' | ||||||
|  | import app from '@adonisjs/core/services/app' | ||||||
| import { middleware } from './kernel.js' | import { middleware } from './kernel.js' | ||||||
| 
 | 
 | ||||||
| transmit.registerRoutes() | transmit.registerRoutes() | ||||||
|  | @ -20,37 +21,13 @@ const AuthController = () => import('#controllers/auth_controller') | ||||||
| router.group(() => { | router.group(() => { | ||||||
|   router.post('/auth/request', [AuthController, 'requestLogin']).use(authThrottle) |   router.post('/auth/request', [AuthController, 'requestLogin']).use(authThrottle) | ||||||
|   router.post('/auth/verify', [AuthController, 'verifyCode']).use(throttle) |   router.post('/auth/verify', [AuthController, 'verifyCode']).use(throttle) | ||||||
|   router.post('/auth/register', [AuthController, 'register']).use(throttle) |  | ||||||
|   // TODO: Magic link login
 |  | ||||||
|   // router.get('/auth/magic-link', 'AuthController.magicLink').use(throttle)
 |   // router.get('/auth/magic-link', 'AuthController.magicLink').use(throttle)
 | ||||||
|   // router.get('/auth/listen', 'AuthController.listen')
 |   // router.get('/auth/listen', 'AuthController.listen')
 | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| const UserController = () => import('#controllers/user_controller') | const UserController = () => import('#controllers/user_controller') | ||||||
| 
 | 
 | ||||||
| router.get('/users/@me', [UserController, 'me']).use(middleware.auth()) | if (app.inDev) { | ||||||
| 
 |   router.post('users', [UserController, 'create']) | ||||||
| // TEST ROUTE
 |  | ||||||
| import redis from '@adonisjs/redis/services/main' |  | ||||||
| 
 |  | ||||||
| router.get('/', async () => { |  | ||||||
|   await redis.publish("jobs_queue", JSON.stringify({ |  | ||||||
|     type: 1, |  | ||||||
|     date: "09/12/2024" |  | ||||||
|   })) |  | ||||||
|   return { message: 'Hello, world!' } |  | ||||||
| }) |  | ||||||
| // END TEST ROUTE
 |  | ||||||
| 
 |  | ||||||
| const CollesController = () => import('#controllers/colles_controller') |  | ||||||
| router.group(() => { |  | ||||||
|   // TODO: PRIVATE ROUTES
 |  | ||||||
|   router.post('/', [CollesController, 'create']) |  | ||||||
|   router.post('/upcoming', [CollesController, 'createUpcoming']) |  | ||||||
|   router.get('/', [CollesController, 'index']).use(middleware.auth()) |  | ||||||
|   router.get('/:colleId', [CollesController, 'show']).use(middleware.auth()) |  | ||||||
|   // router.get('/colles/:id', 'CollesController.show')
 |  | ||||||
|   // router.put('/colles/:id', 'CollesController.update')
 |  | ||||||
|   // router.delete('/colles/:id', 'CollesController.delete')
 |  | ||||||
| } | } | ||||||
| ).prefix('/colles') | router.get('/users/@me', [UserController, 'me']).use(middleware.auth()) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue