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