Compare commits
	
		
			2 commits
		
	
	
		
			73db24d3c0
			...
			c724dac086
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c724dac086 | ||
|   | 2f7373f367 | 
					 28 changed files with 6006 additions and 147 deletions
				
			
		|  | @ -1,5 +1,5 @@ | ||||||
| import type { HttpContext } from '@adonisjs/core/http' | import type { HttpContext } from '@adonisjs/core/http' | ||||||
| import { requestLoginValidator, verifyCodeValidator } from '#validators/auth' | import { registerValidator, 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,10 +7,9 @@ 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) { } | ||||||
| 
 | 
 | ||||||
|   // POST /auth/request
 |   // POST /auth/request
 | ||||||
|   async requestLogin({ request, response, captcha }: HttpContext) { |   async requestLogin({ request, response, captcha }: HttpContext) { | ||||||
|  | @ -19,25 +18,18 @@ 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({ | ||||||
|           message: 'Captcha validation failed', |           success: false, | ||||||
|  |           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.generateToken(email, user.id, expiresIn) |     const payload = await this.authService.generateCode(email, expiresIn) | ||||||
| 
 | 
 | ||||||
|     // Send email
 |     // Send email
 | ||||||
|     await mail |     await mail | ||||||
|  | @ -55,9 +47,6 @@ export default class AuthController { | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       success: true, |       success: true, | ||||||
|       data: { |  | ||||||
|         token: payload.token, |  | ||||||
|       }, |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -65,8 +54,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, userId, email } = await this.authService.validateCode(code) |     const { success, email } = await this.authService.validateCode(code) | ||||||
|     if (!success || !userId || isNaN(userId)) { |     if (!success) { | ||||||
|       return response.badRequest({ |       return response.badRequest({ | ||||||
|         success: false, |         success: false, | ||||||
|         message: 'Code de vérification invalide', |         message: 'Code de vérification invalide', | ||||||
|  | @ -74,32 +63,78 @@ export default class AuthController { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Find user by id
 |     // Find user by id
 | ||||||
|     const user = await User.findBy('id', userId) |     const user = await User.findBy('email', email) | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       return response.notFound({ |       // If the user does not exist, return a token for registration
 | ||||||
|         success: false, |       const expiresIn = '1 hour' | ||||||
|         message: 'Utilisateur non trouvé', |       const { token, email: userEmail } = this.authService.generateToken(email, expiresIn) | ||||||
|       }) |       return { | ||||||
|  |         token, | ||||||
|  |         email: userEmail, | ||||||
|  |         success: true, | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Set user email to the email used to request login (lowercase)
 |  | ||||||
|     user.email = email.toLowerCase() |  | ||||||
|     await user.save() |  | ||||||
| 
 |  | ||||||
|     // 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 user |     return { | ||||||
|  |       success: true, | ||||||
|  |       user | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   magicLink({}: HttpContext) { |   // POST /auth/register
 | ||||||
|     // Validate signed url (adonis)
 |   async register({ request, response, auth }: HttpContext) { | ||||||
|     // + login current device
 |     const { firstName, lastName, className, token } = await request.validateUsing(registerValidator) | ||||||
|     // + SSE to notify other devices (and login)
 | 
 | ||||||
|  |     // 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é.', | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Check if user already exists
 | ||||||
|  |     const existingUser = await User.findBy('email', email) | ||||||
|  |     if (existingUser) { | ||||||
|  |       // 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
 | ||||||
|  |     await auth.use('web').login(user, true) // true for remember me
 | ||||||
|  |     return { | ||||||
|  |       success: true, | ||||||
|  |       user | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   listen({}: HttpContext) { |   // TODO: Magic link login
 | ||||||
|     // Listen for SSE events
 |   // magicLink({ }: HttpContext) {
 | ||||||
|     // Need an AUTH token to connect
 |   //   // Validate signed url (adonis)
 | ||||||
|     // AUTH token sent to client in requestLogin
 |   //   // + 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
 | ||||||
|  |   // }
 | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										168
									
								
								app/controllers/colles_controller.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								app/controllers/colles_controller.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,168 @@ | ||||||
|  | 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,7 +21,8 @@ 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, | ||||||
|       avatar, |       // TODO: No avatar for now!!
 | ||||||
|  |       // 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.message || 'Internal Server Error' |     const message = error.messages || 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, | ||||||
|  |  | ||||||
							
								
								
									
										72
									
								
								app/models/colle.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								app/models/colle.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,72 @@ | ||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								app/models/colle_attachment.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/models/colle_attachment.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								app/models/examiner.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/models/examiner.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | import { BaseModel, column } from '@adonisjs/lucid/orm' | ||||||
|  | 
 | ||||||
|  | export default class Examiner extends BaseModel { | ||||||
|  |   @column({ isPrimary: true }) | ||||||
|  |   declare id: number | ||||||
|  | 
 | ||||||
|  |   @column() | ||||||
|  |   declare name: string | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								app/models/room.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/models/room.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | import { BaseModel, column } from '@adonisjs/lucid/orm' | ||||||
|  | 
 | ||||||
|  | export default class Room extends BaseModel { | ||||||
|  |   @column({ isPrimary: true }) | ||||||
|  |   declare id: number | ||||||
|  | 
 | ||||||
|  |   @column() | ||||||
|  |   declare name: string | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								app/models/subject.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/models/subject.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | 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 } from '@adonisjs/lucid/orm' | import { BaseModel, column, computed } 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,11 +17,13 @@ export default class User extends BaseModel { | ||||||
|   @column() |   @column() | ||||||
|   declare lastName: string |   declare lastName: string | ||||||
| 
 | 
 | ||||||
|   @column() |   @computed() | ||||||
|   declare email: string |   get fullName() { | ||||||
|  |     return `${this.firstName} ${this.lastName}` | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   @column() |   @column() | ||||||
|   declare avatar: string |   declare email: string | ||||||
| 
 | 
 | ||||||
|   @column.dateTime({ autoCreate: true }) |   @column.dateTime({ autoCreate: true }) | ||||||
|   declare createdAt: DateTime |   declare createdAt: DateTime | ||||||
|  |  | ||||||
|  | @ -1,17 +1,13 @@ | ||||||
| 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 generateToken(email: string, userId: number, expiresIn: string) { |   async generateCode(email: string, expiresIn: string) { | ||||||
|     // Generate magic link token
 |     // TODO: Generate magic link token
 | ||||||
|     const identifier = `${email}:${userId}` |     // const identifier = email
 | ||||||
|     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) | ||||||
|  | @ -19,7 +15,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: identifier, |       value: email, | ||||||
|       ttl: expiresIn, |       ttl: expiresIn, | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|  | @ -27,81 +23,52 @@ 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 id = await cache.get({ key }) |     const email = await cache.get({ key }) | ||||||
|     if (!id) return { success: false, userId: null, email: null } |     if (!email) return { success: false, 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) |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   parseNameFromEmail(email: string): Promise<string> { |   generateToken(email: string, expiresIn: string) { | ||||||
|     // Parse name from email
 |     // Generate token
 | ||||||
|     return new Promise((resolve, reject) => { |     const identifier = email.toLowerCase() | ||||||
|       try { |     const token = encryption.encrypt(identifier, expiresIn) | ||||||
|         const [firstName, lastName] = email.split('@')[0].split('.') | 
 | ||||||
|         resolve(`${firstName} ${lastName}`.toLowerCase()) |     return { | ||||||
|       } catch (error) { |       token, | ||||||
|         reject(new Error('Invalid email format')) |       expiresIn, | ||||||
|       } |       email: identifier, | ||||||
|     }) |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   findUserByEmail(email: string) { |   validateToken(token: string) { | ||||||
|     return User.query().where('email', email).first() |     // Decrypt token
 | ||||||
|   } |     const decrypted = encryption.decrypt(token) | ||||||
|  |     if (!decrypted) return { success: false, email: null } | ||||||
| 
 | 
 | ||||||
|   async findUser(email: string) { |     // Validate email format
 | ||||||
|     // Try to find user by email
 |     const email = (decrypted as string).toLowerCase() | ||||||
|     let user: User | null | undefined = await this.findUserByEmail(email) |     if (!email.includes('@')) return { success: false, email: null } | ||||||
|     if (user) return user |  | ||||||
| 
 | 
 | ||||||
|     // If not found, try to parse name from email and find user by name
 |     return { | ||||||
|     const name = await this.parseNameFromEmail(email) |       success: true, | ||||||
|     const users = await User.query() |       email, | ||||||
|       // 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}"` |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										95
									
								
								app/services/colle_service.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								app/services/colle_service.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,95 @@ | ||||||
|  | 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,20 +19,36 @@ export const verifyCodeValidator = vine.compile( | ||||||
|   }) |   }) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| export const magicLinkValidator = vine.compile( | function toTitleCase(value: string) { | ||||||
|  |   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(), | ||||||
|   }) |   }) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| export const listenValidator = vine.compile( | // TODO: Magic link login
 | ||||||
|   vine.object({ | // export const magicLinkValidator = vine.compile(
 | ||||||
|     token: vine.string().uuid(), | //   vine.object({
 | ||||||
|   }) | //     token: vine.string(),
 | ||||||
| ) | //   })
 | ||||||
|  | // )
 | ||||||
| 
 | 
 | ||||||
| export const exchangeTokenValidator = vine.compile( | // export const listenValidator = 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(),
 | ||||||
|  | //   })
 | ||||||
|  | // )
 | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								app/validators/colle.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/validators/colle.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | 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: true, |   origin: '*', | ||||||
|   methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], |   methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], | ||||||
|   headers: true, |   headers: true, | ||||||
|   exposeHeaders: [], |   exposeHeaders: [], | ||||||
|  |  | ||||||
|  | @ -5,12 +5,11 @@ export default class extends BaseSchema { | ||||||
| 
 | 
 | ||||||
|   async up() { |   async up() { | ||||||
|     this.schema.createTable(this.tableName, (table) => { |     this.schema.createTable(this.tableName, (table) => { | ||||||
|       table.increments('id').notNullable() |       table.increments('id').primary() | ||||||
|       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() | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								database/migrations/1751803537457_create_rooms_table.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								database/migrations/1751803537457_create_rooms_table.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | 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) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								database/migrations/1751803556566_create_subjects_table.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								database/migrations/1751803556566_create_subjects_table.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | 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) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								database/migrations/1751803858736_create_examiners_table.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								database/migrations/1751803858736_create_examiners_table.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | 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) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								database/migrations/1751804476638_create_colles_table.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								database/migrations/1751804476638_create_colles_table.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | 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) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | 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,3 +18,18 @@ 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,7 +63,6 @@ | ||||||
|     "@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
									
									
									
										Normal file
									
								
							
							
						
						
									
										5306
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -2,18 +2,15 @@ Bonjour, | ||||||
| 
 | 
 | ||||||
| Nous avons reçu une demande de connexion à votre compte Khollisé. | Nous avons reçu une demande de connexion à votre compte Khollisé. | ||||||
| 
 | 
 | ||||||
| Cliquez sur ce lien pour vous connecter : | Utilisez ce code de vérification à 6 chiffres pour vous connecter : | ||||||
| {{ magicLink }} |  | ||||||
| 
 |  | ||||||
| Vous pouvez également entrer ce code de vérification à 6 chiffres : |  | ||||||
| {{ formattedOTP }} | {{ formattedOTP }} | ||||||
| 
 | 
 | ||||||
| Ce lien et ce code expireront dans {{ expiresIn }} et ne peuvent être utilisés qu'une seule fois. | Ce code expirera dans {{ expiresIn }} et ne peut être utilisé qu'une seule fois. | ||||||
| 
 | 
 | ||||||
| Si vous n'avez pas demandé cette connexion, vous pouvez ignorer cet e-mail. | Si vous n'avez pas demandé cette connexion, vous pouvez ignorer cet email. | ||||||
| 
 | 
 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| Cet e-mail a été envoyé à {{ email }} | Cet email a été envoyé à {{ email }} | ||||||
| 
 | 
 | ||||||
| © {{ new Date().getFullYear() }} Khollisé. Tous droits réservés. | © {{ new Date().getFullYear() }} Khollisé. Tous droits réservés. | ||||||
|  |  | ||||||
|  | @ -11,17 +11,7 @@ | ||||||
|         <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 bouton ci-dessous pour vous connecter instantanément : |           Nous avons reçu une demande de connexion à votre compte Khollisé. Utilisez le code de vérification ci-dessous pour vous connecter : | ||||||
|         </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;"> | ||||||
|  | @ -31,7 +21,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 lien et le code ci-dessus expireront dans {{ expiresIn }} et ne peuvent être utilisés qu'une seule fois. |           Le code ci-dessus expirera dans {{ expiresIn }} et ne peut être utilisé 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;"> | ||||||
|  |  | ||||||
|  | @ -13,4 +13,4 @@ import limiter from '@adonisjs/limiter/services/main' | ||||||
| 
 | 
 | ||||||
| export const throttle = limiter.define('global', () => { | export const throttle = limiter.define('global', () => { | ||||||
|   return limiter.allowRequests(10).every('1 minute') |   return limiter.allowRequests(10).every('1 minute') | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | @ -11,7 +11,6 @@ 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() | ||||||
|  | @ -21,13 +20,37 @@ 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') | ||||||
| 
 | 
 | ||||||
| if (app.inDev) { |  | ||||||
|   router.post('users', [UserController, 'create']) |  | ||||||
| } |  | ||||||
| router.get('/users/@me', [UserController, 'me']).use(middleware.auth()) | router.get('/users/@me', [UserController, 'me']).use(middleware.auth()) | ||||||
|  | 
 | ||||||
|  | // 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') | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue