feat: new register flow

This commit is contained in:
Nathan Lamy 2025-07-30 17:44:30 +02:00
parent 5598b40d66
commit cc63e16d9c
4 changed files with 52 additions and 36 deletions

View file

@ -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,59 +73,83 @@ 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) 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()
// TODO: Rewrite user creation (NEVER CREATE USER - use string similarity)
// Create new user
const user = await User.create({
firstName,
lastName,
className,
email,
})
// Perform login // Perform login
await auth.use('web').login(user, true) // true for remember me await auth.use('web').login(user, true) // true for remember me
return { return {
success: true, success: true,
user,
} }
} }
// POST /auth/logout // POST /auth/logout
async logout({ auth }: HttpContext) { async logout({ auth }: HttpContext) {
// Logout user
await auth.use('web').logout() await auth.use('web').logout()
return { return {
success: true, success: true,

View file

@ -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()

View file

@ -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(),
}) })

View file

@ -22,6 +22,7 @@ router.group(() => {
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.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')