Compare commits
	
		
			4 commits
		
	
	
		
			c724dac086
			...
			1082f29143
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1082f29143 | ||
|   | cc63e16d9c | ||
|   | 5598b40d66 | ||
|   | a6b8669e3d | 
					 6 changed files with 118 additions and 39 deletions
				
			
		|  | @ -31,7 +31,6 @@ export default class AuthController { | ||||||
|     const expiresIn = '15 minutes' |     const expiresIn = '15 minutes' | ||||||
|     const payload = await this.authService.generateCode(email, expiresIn) |     const payload = await this.authService.generateCode(email, expiresIn) | ||||||
| 
 | 
 | ||||||
|     // Send email
 |  | ||||||
|     await mail |     await mail | ||||||
|       .send((message) => { |       .send((message) => { | ||||||
|         message |         message | ||||||
|  | @ -62,7 +61,6 @@ export default class AuthController { | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Find user by id
 |  | ||||||
|     const user = await User.findBy('email', email) |     const user = await User.findBy('email', email) | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       // If the user does not exist, return a token for registration
 |       // If the user does not exist, return a token for registration
 | ||||||
|  | @ -75,53 +73,86 @@ export default class AuthController { | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // 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 { | ||||||
|       success: true, |       success: true, | ||||||
|       user |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // GET /auth/autocomplete
 | ||||||
|  |   async listNames({ request }: HttpContext) { | ||||||
|  |     const { className } = request.qs() | ||||||
|  |     if (!className) { | ||||||
|  |       return { | ||||||
|  |         success: false, | ||||||
|  |         message: 'Veuillez spécifier une classe', | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return User.query() | ||||||
|  |       .select('firstName', 'lastName') | ||||||
|  |       .where('className', className) | ||||||
|  |       .orderBy('lastName', 'asc') | ||||||
|  |       .then((users) => { | ||||||
|  |         return { | ||||||
|  |           success: true, | ||||||
|  |           data: users.map((user) => ({ | ||||||
|  |             value: `${user.firstName}::${user.lastName}`, | ||||||
|  |             label: user.fullName, | ||||||
|  |           })), | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   // POST /auth/register
 |   // POST /auth/register
 | ||||||
|   async register({ request, response, auth }: HttpContext) { |   async register({ request, response, auth }: HttpContext) { | ||||||
|     const { firstName, lastName, className, token } = await request.validateUsing(registerValidator) |     const { name, className, token } = await request.validateUsing(registerValidator) | ||||||
| 
 | 
 | ||||||
|     // Validate token
 |     // Validate token
 | ||||||
|     const { success, email } = this.authService.validateToken(token) |     const { success, email } = this.authService.validateToken(token) | ||||||
|     if (!success || !email) { |     const [firstName, lastName] = name.split('::') | ||||||
|  |     if (!success || !email || !firstName || !lastName) { | ||||||
|       return response.badRequest({ |       return response.badRequest({ | ||||||
|         success: false, |         success: false, | ||||||
|         message: 'Votre lien de connexion est invalide ou a expiré.', |         message: 'Votre lien de connexion est invalide ou a expiré.', | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Check if user already exists
 |     const user = await User.query() | ||||||
|     const existingUser = await User.findBy('email', email) |       .where('firstName', firstName) | ||||||
|     if (existingUser) { |       .where('lastName', lastName) | ||||||
|       // If user already exists, perform login
 |       .where('className', className) | ||||||
|       await auth.use('web').login(existingUser, true) // true for remember me
 |       .first() | ||||||
|       return { |     if (!user) { | ||||||
|         success: true, |       return response.badRequest({ | ||||||
|         user: existingUser, |         success: false, | ||||||
|       } |         message: 'Utilisateur non trouvé. Veuillez vérifier vos informations.', | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // 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 |  | ||||||
|       }) |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (user.email && user.email.toLowerCase() !== email.toLowerCase()) { | ||||||
|  |       return response.badRequest({ | ||||||
|  |         success: false, | ||||||
|  |         message: | ||||||
|  |           "L'email associé à votre compte ne correspond pas à celui utilisé pour la connexion.", | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     user.email = email.toLowerCase() // Update email if necessary
 | ||||||
|  |     await user.save() | ||||||
|  | 
 | ||||||
|     // Perform login
 |     // 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 { | ||||||
|       success: true, |       success: true, | ||||||
|       user |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // POST /auth/logout
 | ||||||
|  |   async logout({ auth }: HttpContext) { | ||||||
|  |     await auth.use('web').logout() | ||||||
|  |     return { | ||||||
|  |       success: true, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -43,13 +43,17 @@ export default class CollesController { | ||||||
|       .preload('subject') |       .preload('subject') | ||||||
|       .preload('room') |       .preload('room') | ||||||
|       .first() |       .first() | ||||||
|     // TODO: Include BJID and BJSecret !
 |  | ||||||
|     if (!colle) { |     if (!colle) { | ||||||
|       return response.notFound({ message: 'Colle not found' }) |       return response.notFound({ message: 'Colle not found' }) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|       success: true, |       success: true, | ||||||
|       data: colle, |       data: { | ||||||
|  |         ...colle.serialize(), | ||||||
|  |         bjid: colle.bjid, | ||||||
|  |         bjsecret: colle.bjsecret, | ||||||
|  |       }, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -67,6 +71,14 @@ export default class CollesController { | ||||||
|       return response.badRequest({ message: 'Invalid date format' }) |       return response.badRequest({ message: 'Invalid date format' }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Prepare the content and comment for rendering
 | ||||||
|  |     if (payload.comment) { | ||||||
|  |       payload.comment = this.service.prepareHtmlForRendering(payload.comment) | ||||||
|  |     } | ||||||
|  |     if (payload.content) { | ||||||
|  |       payload.content = this.service.prepareHtmlForRendering(payload.content) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const colleData = { |     const colleData = { | ||||||
|       studentId: student.id, |       studentId: student.id, | ||||||
|       examinerId: examiner.id, |       examinerId: examiner.id, | ||||||
|  |  | ||||||
|  | @ -11,10 +11,10 @@ export default class User extends BaseModel { | ||||||
|   @column() |   @column() | ||||||
|   declare className: string |   declare className: string | ||||||
| 
 | 
 | ||||||
|   @column() |   @column({ serializeAs: null}) | ||||||
|   declare firstName: string |   declare firstName: string | ||||||
| 
 | 
 | ||||||
|   @column() |   @column({ serializeAs: null}) | ||||||
|   declare lastName: string |   declare lastName: string | ||||||
| 
 | 
 | ||||||
|   @computed() |   @computed() | ||||||
|  | @ -22,7 +22,7 @@ export default class User extends BaseModel { | ||||||
|     return `${this.firstName} ${this.lastName}` |     return `${this.firstName} ${this.lastName}` | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @column() |   @column({ serializeAs: null }) | ||||||
|   declare email: string |   declare email: string | ||||||
| 
 | 
 | ||||||
|   @column.dateTime({ autoCreate: true }) |   @column.dateTime({ autoCreate: true }) | ||||||
|  |  | ||||||
|  | @ -92,4 +92,45 @@ export class ColleService { | ||||||
|       favoriteColles, |       favoriteColles, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // Pre-process HTML content to render LaTeX
 | ||||||
|  |   private removeTrailingLines(htmlString: string) { | ||||||
|  |     return htmlString.replace(/(<br\s*\/?>\s*)+$/gi, '').trim() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private extractLatexImages(html: string) { | ||||||
|  |     const imgRegex = /<img[^>]+src="(https:\/\/latex\.codecogs\.com\/gif\.latex\?(=?.*?))"[^>]*>/g | ||||||
|  |     let parts = [] | ||||||
|  |     let latexMatches: string[] = [] | ||||||
|  |     let lastIndex = 0 | ||||||
|  | 
 | ||||||
|  |     html.replace(imgRegex, (match, _, latex, index) => { | ||||||
|  |       parts.push(html.slice(lastIndex, index)) // Add HTML before image
 | ||||||
|  |       latexMatches.push(decodeURIComponent(latex)) // Extract and decode LaTeX
 | ||||||
|  |       lastIndex = index + match.length | ||||||
|  |       return '' | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     parts.push(html.slice(lastIndex)) // Add remaining HTML after last image
 | ||||||
|  | 
 | ||||||
|  |     return { parts, latexMatches } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   prepareHtmlForRendering(html: string) { | ||||||
|  |     const strippedHtml = this.removeTrailingLines(html) | ||||||
|  |     const { parts, latexMatches } = this.extractLatexImages(strippedHtml) | ||||||
|  | 
 | ||||||
|  |     const outputHtml = parts | ||||||
|  |       .map((part, i) => { | ||||||
|  |         if (!latexMatches[i]) { | ||||||
|  |           return part | ||||||
|  |         } | ||||||
|  |         return `${part}$$${latexMatches[i]}$$` | ||||||
|  |       }) | ||||||
|  |       .join('') | ||||||
|  | 
 | ||||||
|  |     // Remove all "\," from string
 | ||||||
|  |     const regex = /\\,/g | ||||||
|  |     return outputHtml.replace(regex, ' ') | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,16 +19,9 @@ export const verifyCodeValidator = 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( | export const registerValidator = vine.compile( | ||||||
|   vine.object({ |   vine.object({ | ||||||
|     firstName: vine.string().minLength(2).maxLength(50).trim().transform(toTitleCase), |     name: vine.string().minLength(2).maxLength(50).trim(), | ||||||
|     lastName: vine.string().minLength(2).maxLength(50).trim().toUpperCase(), |  | ||||||
|     className: vine.string().minLength(2).maxLength(50), |     className: vine.string().minLength(2).maxLength(50), | ||||||
|     token: vine.string(), |     token: vine.string(), | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  | @ -21,6 +21,8 @@ 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) |   router.post('/auth/register', [AuthController, 'register']).use(throttle) | ||||||
|  |   router.post('/auth/logout', [AuthController, 'logout']) | ||||||
|  |   router.get('/auth/autocomplete', [AuthController, 'listNames']).use(throttle) | ||||||
|   // TODO: Magic link login
 |   // 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')
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue