feat: add colles routes

This commit is contained in:
Nathan Lamy 2025-07-30 12:56:52 +02:00
parent 2f7373f367
commit c724dac086
19 changed files with 502 additions and 170 deletions

View file

@ -1,5 +1,5 @@
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 { AuthService } from '#services/auth_service'
import { inject } from '@adonisjs/core'
@ -7,10 +7,9 @@ import app from '@adonisjs/core/services/app'
import env from '#start/env'
import User from '#models/user'
// TODO: When login, set user.email to the email used to request login (lowercase)
@inject()
export default class AuthController {
constructor(private authService: AuthService) {}
constructor(private authService: AuthService) { }
// POST /auth/request
async requestLogin({ request, response, captcha }: HttpContext) {
@ -19,25 +18,18 @@ export default class AuthController {
const validateResult = await (captcha.use('turnstile') as any).validate()
if (!validateResult.success) {
return response.badRequest({
message: 'Captcha validation failed',
success: false,
message: 'Veuillez valider le captcha',
error: validateResult.errorCodes,
})
}
}
// Find user by email
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
const expiresIn = '15 minutes'
const payload = await this.authService.generateToken(email, user.id, expiresIn)
const payload = await this.authService.generateCode(email, expiresIn)
// Send email
await mail
@ -55,9 +47,6 @@ export default class AuthController {
return {
success: true,
data: {
token: payload.token,
},
}
}
@ -65,8 +54,8 @@ export default class AuthController {
async verifyCode({ request, response, auth }: HttpContext) {
// Validate code
const { code } = await request.validateUsing(verifyCodeValidator)
const { success, userId, email } = await this.authService.validateCode(code)
if (!success || !userId || isNaN(userId)) {
const { success, email } = await this.authService.validateCode(code)
if (!success) {
return response.badRequest({
success: false,
message: 'Code de vérification invalide',
@ -74,32 +63,78 @@ export default class AuthController {
}
// Find user by id
const user = await User.findBy('id', userId)
const user = await User.findBy('email', email)
if (!user) {
return response.notFound({
success: false,
message: 'Utilisateur non trouvé',
})
// If the user does not exist, return a token for registration
const expiresIn = '1 hour'
const { token, email: userEmail } = this.authService.generateToken(email, expiresIn)
return {
token,
email: userEmail,
success: true,
}
}
// Set user email to the email used to request login (lowercase)
user.email = email.toLowerCase()
await user.save()
// Perform login
await auth.use('web').login(user, true) // true for remember me
return user
return {
success: true,
user
}
}
magicLink({}: HttpContext) {
// Validate signed url (adonis)
// + login current device
// + SSE to notify other devices (and login)
// POST /auth/register
async register({ request, response, auth }: HttpContext) {
const { firstName, lastName, className, token } = await request.validateUsing(registerValidator)
// Validate token
const { success, email } = this.authService.validateToken(token)
if (!success || !email) {
return response.badRequest({
success: false,
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) {
// Listen for SSE events
// Need an AUTH token to connect
// AUTH token sent to client in requestLogin
}
// TODO: Magic link login
// magicLink({ }: HttpContext) {
// // Validate signed url (adonis)
// // + login current device
// // + SSE to notify other devices (and login)
// }
// listen({ }: HttpContext) {
// // Listen for SSE events
// // Need an AUTH token to connect
// // AUTH token sent to client in requestLogin
// }
}

View 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`)
}
}

View file

@ -21,7 +21,8 @@ export default class UserController {
// const avatar = await drive.use().getSignedUrl(key)
return User.create({
...payload,
avatar,
// TODO: No avatar for now!!
// avatar,
})
}
}

View file

@ -20,7 +20,7 @@ export default class HttpExceptionHandler extends ExceptionHandler {
*/
async handle(error: any, ctx: HttpContext) {
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 response = {
status: statusCode,

View file

@ -1,7 +1,7 @@
import { DateTime } from 'luxon'
import { BaseModel, column, hasMany, hasOne } from '@adonisjs/lucid/orm'
import { BaseModel, belongsTo, column, hasMany } from '@adonisjs/lucid/orm'
import User from './user.js'
import type { HasMany, HasOne } from '@adonisjs/lucid/types/relations'
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'
@ -11,25 +11,42 @@ export default class Colle extends BaseModel {
@column({ isPrimary: true })
declare id: number
@hasOne(() => User)
declare student: HasOne<typeof User>
@belongsTo(() => User, { foreignKey: 'studentId' })
declare student: BelongsTo<typeof User>
@hasOne(() => Examiner)
declare examiner: HasOne<typeof Examiner>
@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()
@column({ serializeAs: null })
declare bjsecret: string
@column()
@column({ serializeAs: null })
declare bjid: string
// Colle data
@hasOne(() => Subject)
declare subject: HasOne<typeof Subject>
@belongsTo(() => Subject)
declare subject: BelongsTo<typeof Subject>
@hasOne(() => Room)
declare room: HasOne<typeof Room>
@column({ serializeAs: null })
declare subjectId: number
@belongsTo(() => Room)
declare room: BelongsTo<typeof Room>
@column({ serializeAs: null })
declare roomId: number
@column()
declare grade: number

View file

@ -1,5 +1,5 @@
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'
export default class User extends BaseModel {
@ -17,11 +17,13 @@ export default class User extends BaseModel {
@column()
declare lastName: string
@column()
declare email: string
@computed()
get fullName() {
return `${this.firstName} ${this.lastName}`
}
@column()
declare avatar: string
declare email: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

View file

@ -1,17 +1,13 @@
import encryption from '@adonisjs/core/services/encryption'
import cache from '@adonisjs/cache/services/main'
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 {
async generateToken(email: string, userId: number, expiresIn: string) {
// Generate magic link token
const identifier = `${email}:${userId}`
const token = encryption.encrypt(identifier, expiresIn)
const magicLink = env.get('PUBLIC_AUTH_URL') + '/success?signature=' + token
async generateCode(email: string, expiresIn: string) {
// TODO: Generate magic link token
// const identifier = email
// const token = encryption.encrypt(identifier, expiresIn)
// const magicLink = env.get('PUBLIC_AUTH_URL') + '/success?signature=' + token
// Generate code
const formattedOTP = Math.floor(Math.random() * 1000000)
@ -19,7 +15,7 @@ export class AuthService {
.padStart(6, '0')
await cache.set({
key: 'auth:otp:' + formattedOTP,
value: identifier,
value: email,
ttl: expiresIn,
})
@ -27,81 +23,52 @@ export class AuthService {
return {
emailTitle,
formattedOTP,
magicLink,
// magicLink,
expiresIn,
email,
token,
}
}
parseIdentifier(id: string) {
const [email, userId] = id.split(':')
return {
email: email.toLowerCase(),
userId: parseInt(userId, 10),
// token,
}
}
async validateCode(code: string) {
// Validate code
const key = 'auth:otp:' + code
const id = await cache.get({ key })
if (!id) return { success: false, userId: null, email: null }
const email = await cache.get({ key })
if (!email) return { success: false, email: null }
// Delete code from cache
await cache.delete({ key })
return {
email,
success: true,
...this.parseIdentifier(id)
}
}
parseNameFromEmail(email: string): Promise<string> {
// Parse name from email
return new Promise((resolve, reject) => {
try {
const [firstName, lastName] = email.split('@')[0].split('.')
resolve(`${firstName} ${lastName}`.toLowerCase())
} catch (error) {
reject(new Error('Invalid email format'))
}
})
generateToken(email: string, expiresIn: string) {
// Generate token
const identifier = email.toLowerCase()
const token = encryption.encrypt(identifier, expiresIn)
return {
token,
expiresIn,
email: identifier,
}
}
findUserByEmail(email: string) {
return User.query().where('email', email).first()
}
validateToken(token: string) {
// Decrypt token
const decrypted = encryption.decrypt(token)
if (!decrypted) return { success: false, email: null }
async findUser(email: string) {
// Try to find user by email
let user: User | null | undefined = await this.findUserByEmail(email)
if (user) return user
// Validate email format
const email = (decrypted as string).toLowerCase()
if (!email.includes('@')) return { success: false, email: null }
// If not found, try to parse name from email and find user by name
const name = await this.parseNameFromEmail(email)
const users = await User.query()
// Select all users with no email
.whereNull('email')
const names = users.map((user) => `${user.firstName} ${user.lastName}`.toLowerCase())
// Search for similar names
const data = await cmp.searchAsync(name, names)
if (data.length === 0) {
console.warn(`No user found for email "${email}" or name "${name}"`)
return null
return {
success: true,
email,
}
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}"`
)
}
}

View 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,
}
}
}

View file

@ -9,7 +9,7 @@ export const requestLoginValidator = vine.compile(
})
.normalizeEmail({
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({
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(),
})
)
export const listenValidator = vine.compile(
vine.object({
token: vine.string().uuid(),
})
)
// TODO: Magic link login
// export const magicLinkValidator = vine.compile(
// vine.object({
// token: vine.string(),
// })
// )
export const exchangeTokenValidator = vine.compile(
vine.object({
token: vine.string().uuid(),
})
)
// export const listenValidator = vine.compile(
// vine.object({
// token: vine.string().uuid(),
// })
// )
// export const exchangeTokenValidator = vine.compile(
// vine.object({
// token: vine.string().uuid(),
// })
// )

30
app/validators/colle.ts Normal file
View 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,
}),
)

View file

@ -9,7 +9,7 @@ import { defineConfig } from '@adonisjs/cors'
const corsConfig = defineConfig({
enabled: true,
// TODO: Only same domain
origin: true,
origin: '*',
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
headers: true,
exposeHeaders: [],

View file

@ -10,7 +10,6 @@ export default class extends BaseSchema {
table.string('first_name', 50).notNullable()
table.string('last_name', 50).notNullable()
table.string('email', 254).unique().nullable()
table.string('avatar', 254).notNullable()
table.timestamp('created_at').notNullable()
table.timestamp('updated_at').nullable()
})

View file

@ -18,3 +18,18 @@ services:
- ./data:/var/lib/postgresql/data
ports:
- "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:

View file

@ -63,7 +63,6 @@
"@adonisjs/transmit": "^2.0.2",
"@vinejs/vine": "^3.0.1",
"adonis-captcha-guard": "^1.0.1",
"cmpstr": "^3.0.1",
"edge.js": "^6.2.1",
"luxon": "^3.6.1",
"pg": "^8.16.0",

9
pnpm-lock.yaml generated
View file

@ -47,9 +47,6 @@ importers:
adonis-captcha-guard:
specifier: ^1.0.1
version: 1.0.1(@adonisjs/core@6.19.0(@adonisjs/assembler@7.8.2(typescript@5.8.3))(@vinejs/vine@3.0.1)(edge.js@6.2.1))
cmpstr:
specifier: ^3.0.1
version: 3.0.1
edge.js:
specifier: ^6.2.1
version: 6.2.1
@ -1114,10 +1111,6 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
cmpstr@3.0.1:
resolution: {integrity: sha512-0gj7U07fS8XaAx4IY854d6xpttqxVKUtaeI/giOguqDrXD9OQaBlW321NwhoE+WCF7rv+Pj55YL5kEW3jVuxEA==}
engines: {node: '>=18.0.0'}
code-block-writer@13.0.3:
resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==}
@ -3858,8 +3851,6 @@ snapshots:
cluster-key-slot@1.1.2: {}
cmpstr@3.0.1: {}
code-block-writer@13.0.3: {}
color-convert@2.0.1:

View file

@ -2,18 +2,15 @@ Bonjour,
Nous avons reçu une demande de connexion à votre compte Khollisé.
Cliquez sur ce lien pour vous connecter :
{{ magicLink }}
Vous pouvez également entrer ce code de vérification à 6 chiffres :
Utilisez ce code de vérification à 6 chiffres pour vous connecter :
{{ 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.

View file

@ -11,17 +11,7 @@
<p style="margin-bottom: 16px;">Bonjour,</p>
<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 :
</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 :
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="margin: 24px 0; text-align: center;">
@ -31,7 +21,7 @@
</div>
<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 style="font-size: 14px; color: #6b7280; font-style: italic; margin-bottom: 16px;">

View file

@ -13,4 +13,4 @@ import limiter from '@adonisjs/limiter/services/main'
export const throttle = limiter.define('global', () => {
return limiter.allowRequests(10).every('1 minute')
})
})

View file

@ -11,7 +11,6 @@ import router from '@adonisjs/core/services/router'
import transmit from '@adonisjs/transmit/services/main'
import { authThrottle } from './limiters.js'
import { throttle } from './limiter.js'
import app from '@adonisjs/core/services/app'
import { middleware } from './kernel.js'
transmit.registerRoutes()
@ -21,26 +20,37 @@ const AuthController = () => import('#controllers/auth_controller')
router.group(() => {
router.post('/auth/request', [AuthController, 'requestLogin']).use(authThrottle)
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/listen', 'AuthController.listen')
})
const UserController = () => import('#controllers/user_controller')
if (app.inDev) {
router.post('users', [UserController, 'create'])
}
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: 2
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')