Introduce request login

This commit is contained in:
Nathan 2025-05-19 13:25:42 +02:00
parent e4a802d5f2
commit 709a7bb6f7
16 changed files with 452 additions and 27 deletions

View file

@ -4,6 +4,7 @@ HOST=localhost
LOG_LEVEL=info
APP_KEY=
NODE_ENV=development
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=root
@ -11,10 +12,15 @@ DB_PASSWORD=root
DB_DATABASE=app
LIMITER_STORE=redis
DRIVE_DISK=fs
SMTP_HOST=
SMTP_PORT=
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
TURNSTILE_SITE_KEY=
TURNSTILE_SECRET=
PUBLIC_AUTH_URL=

View file

@ -50,7 +50,11 @@ export default defineConfig({
() => import('@adonisjs/cache/cache_provider'),
() => import('@adonisjs/drive/drive_provider'),
() => import('@adonisjs/mail/mail_provider'),
() => import('@adonisjs/redis/redis_provider')
() => import('@adonisjs/redis/redis_provider'),
() => import('@adonisjs/transmit/transmit_provider'),
() => import('@adonisjs/core/providers/vinejs_provider'),
() => import('adonis-captcha-guard/providers/captcha_provider'),
() => import('@adonisjs/core/providers/edge_provider')
],
/*
@ -87,4 +91,8 @@ export default defineConfig({
],
forceExit: false,
},
metaFiles: [{
pattern: 'resources/views/**/*.edge',
reloadServer: false,
}]
})

View file

@ -0,0 +1,76 @@
import type { HttpContext } from '@adonisjs/core/http'
import { requestLoginValidator, verifyCodeValidator } from '#validators/auth'
import mail from '@adonisjs/mail/services/main'
import { AuthService } from '#services/auth_service'
import { inject } from '@adonisjs/core'
import app from '@adonisjs/core/services/app'
import env from '#start/env'
@inject()
export default class AuthController {
constructor(private authService: AuthService) {}
// POST /auth/request
async requestLogin({ request, response, captcha }: HttpContext) {
// Validate captcha
if (app.inProduction) {
const validateResult = await (captcha.use('turnstile') as any).validate()
if (!validateResult.success) {
return response.badRequest({
message: 'Captcha validation failed',
error: validateResult.errorCodes,
})
}
}
// Generate token
const expiresIn = '15 minutes'
const { email } = await request.validateUsing(requestLoginValidator)
const payload = await this.authService.generateToken(email, expiresIn)
// Send email
await mail.send((message) => {
message
.from(env.get('MAIL_FROM')!)
.to(email)
.subject(payload.emailTitle)
.htmlView('mails/auth', payload)
.textView('mails/auth-fallback', payload)
}).then(console.log).catch(console.error)
return {
success: true,
data: {
token: payload.token,
},
}
}
// POST /auth/verify
async verifyCode({ request }: HttpContext) {
// Validate code
const { code } = await request.validateUsing(verifyCodeValidator)
const email = await this.authService.validateCode(code)
if (!email) {
return {
success: false,
message: 'Le code est invalide ou a expiré',
}
}
// TOOD: Login
// Find user by email (string similary)
}
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

@ -1,4 +1,3 @@
import app from '@adonisjs/core/services/app'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
@ -6,21 +5,29 @@ export default class HttpExceptionHandler extends ExceptionHandler {
* In debug mode, the exception handler will display verbose errors
* with pretty printed stack traces.
*/
protected debug = !app.inProduction
protected debug = false
/**
* Status pages are used to display a custom HTML pages for certain error
* codes. You might want to enable them in production only, but feel
* free to enable them in development as well.
*/
protected renderStatusPages = app.inProduction
protected renderStatusPages = false
/**
* The method is used for handling errors and returning
* response to the client
*/
async handle(error: unknown, ctx: HttpContext) {
return super.handle(error, ctx)
async handle(error: any, ctx: HttpContext) {
const statusCode = error.status || error.statusCode || 500
const message = error.message || 'Internal Server Error'
const stack = this.debug ? error.stack : undefined
const response = {
status: statusCode,
error: message,
stack,
}
return ctx.response.status(statusCode).json(response)
}
/**

View file

@ -0,0 +1,43 @@
import encryption from '@adonisjs/core/services/encryption'
import cache from '@adonisjs/cache/services/main'
import env from '#start/env'
export class AuthService {
async generateToken(email: string, expiresIn: string) {
// Generate magic link token
const token = encryption.encrypt(email, expiresIn)
const magicLink = env.get('PUBLIC_AUTH_URL') + '/success?signature=' + token
// Generate code
const formattedOTP = Math.floor(Math.random() * 1000000)
.toString()
.padStart(6, '0')
await cache.set({
key: 'auth:otp:' + formattedOTP,
value: email,
ttl: expiresIn,
})
const emailTitle = `${formattedOTP} est votre code de vérification pour ${env.get('APP_NAME')}`
return {
emailTitle,
formattedOTP,
magicLink,
expiresIn,
email,
token,
}
}
async validateCode(code: string) {
// Validate code
const key = 'auth:otp:' + code
const email = await cache.get({ key })
if (!email) return false
// Delete code from cache
await cache.delete({ key })
return email
}
}

38
app/validators/auth.ts Normal file
View file

@ -0,0 +1,38 @@
import vine from '@vinejs/vine'
export const requestLoginValidator = vine.compile(
vine.object({
email: vine
.string()
.email({
host_whitelist: ['bginette.fr'],
})
.normalizeEmail({
all_lowercase: true,
}),
})
)
export const verifyCodeValidator = vine.compile(
vine.object({
code: vine.string().minLength(6).maxLength(6),
})
)
export const magicLinkValidator = vine.compile(
vine.object({
token: vine.string(),
})
)
export const listenValidator = vine.compile(
vine.object({
token: vine.string().uuid(),
})
)
export const exchangeTokenValidator = vine.compile(
vine.object({
token: vine.string().uuid(),
})
)

19
config/captcha.ts Normal file
View file

@ -0,0 +1,19 @@
import env from '#start/env'
import { defineConfig, services } from 'adonis-captcha-guard'
const captchaConfig = defineConfig({
turnstile: services.turnstile({
siteKey: env.get('TURNSTILE_SITE_KEY')!,
secret: env.get('TURNSTILE_SECRET')!,
}),
recaptcha: services.recaptcha({
siteKey: env.get('RECAPTCHA_SITE_KEY')!,
secret: env.get('RECAPTCHA_SECRET')!,
}),
})
export default captchaConfig
declare module '@adonisjs/core/types' {
interface CaptchaProviders {}
}

6
config/transmit.ts Normal file
View file

@ -0,0 +1,6 @@
import { defineConfig } from '@adonisjs/transmit'
export default defineConfig({
pingInterval: false,
transport: null,
})

7
docker-compose.yml Normal file
View file

@ -0,0 +1,7 @@
services:
redis:
image: redis
container_name: khollise-redis
restart: unless-stopped
ports:
- "6379:6379"

147
package-lock.json generated
View file

@ -18,6 +18,10 @@
"@adonisjs/lucid": "^21.6.1",
"@adonisjs/mail": "^9.2.2",
"@adonisjs/redis": "^9.2.0",
"@adonisjs/transmit": "^2.0.2",
"@vinejs/vine": "^3.0.1",
"adonis-captcha-guard": "^1.0.1",
"edge.js": "^6.2.1",
"luxon": "^3.6.1",
"pg": "^8.16.0",
"reflect-metadata": "^0.2.2"
@ -694,6 +698,21 @@
"node": ">=18.16.0"
}
},
"node_modules/@adonisjs/transmit": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@adonisjs/transmit/-/transmit-2.0.2.tgz",
"integrity": "sha512-Oyh4S1773N1Ams9mmFM2pplLx8R0IdYwqSTBLRgeDWaR+cyWrL9ZQvF2q7GqIjNzZsXuDgMdPuLlNo+69zIZIg==",
"license": "MIT",
"dependencies": {
"@boringnode/transmit": "^0.2.1"
},
"engines": {
"node": ">=20.11.1"
},
"peerDependencies": {
"@adonisjs/core": "^6.2.0"
}
},
"node_modules/@adonisjs/tsconfig": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@adonisjs/tsconfig/-/tsconfig-1.4.0.tgz",
@ -770,6 +789,21 @@
}
}
},
"node_modules/@boringnode/transmit": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@boringnode/transmit/-/transmit-0.2.2.tgz",
"integrity": "sha512-xIBg5PKqqgawNsXffq1P+BpRDjplfwOspwcFH5wfSN6uVcd5hYGC9lJpcXa1oL3wkQZimtiJhrTHhZZtgh2lqA==",
"license": "MIT",
"dependencies": {
"@boringnode/bus": "^0.7.0",
"@poppinss/utils": "^6.8.3",
"emittery": "^1.0.3",
"matchit": "^1.1.0"
},
"engines": {
"node": ">=20.11.1"
}
},
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
@ -1461,7 +1495,6 @@
"resolved": "https://registry.npmjs.org/@poppinss/inspect/-/inspect-1.0.1.tgz",
"integrity": "sha512-kLeEaBSGhlleyYvKc7c9s3uE6xv7cwyulE0EgHf4jU/CL96h0yC4mkdw1wvC1l1PYYQozCGy46FwMBAAMOobCA==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -2027,6 +2060,12 @@
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"license": "MIT"
},
"node_modules/@types/validator": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz",
"integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
@ -2217,6 +2256,34 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vinejs/compiler": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vinejs/compiler/-/compiler-3.0.0.tgz",
"integrity": "sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@vinejs/vine": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-3.0.1.tgz",
"integrity": "sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ==",
"license": "MIT",
"dependencies": {
"@poppinss/macroable": "^1.0.4",
"@types/validator": "^13.12.2",
"@vinejs/compiler": "^3.0.0",
"camelcase": "^8.0.0",
"dayjs": "^1.11.13",
"dlv": "^1.1.3",
"normalize-url": "^8.0.1",
"validator": "^13.12.0"
},
"engines": {
"node": ">=18.16.0"
}
},
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@ -2292,6 +2359,22 @@
"node": ">=0.4.0"
}
},
"node_modules/adonis-captcha-guard": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/adonis-captcha-guard/-/adonis-captcha-guard-1.0.1.tgz",
"integrity": "sha512-+KkQeE1sNEgdfcX+hR+7pzl9Hib1u3XOdOHJ4PEYpYsLc+qD87LaLg8mDElDUSxAoH/Z5AlSeTr0Gsq5n5VXQQ==",
"license": "MIT",
"dependencies": {
"@poppinss/utils": "^6.7.3",
"got": "^14.2.1"
},
"engines": {
"node": ">=20.6.0"
},
"peerDependencies": {
"@adonisjs/core": "^6.2.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -2396,7 +2479,6 @@
"resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
"integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",
"license": "MIT",
"peer": true,
"bin": {
"astring": "bin/astring"
}
@ -2655,6 +2737,18 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
"integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
@ -2804,8 +2898,7 @@
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/clean-regexp": {
"version": "1.0.0",
@ -3124,6 +3217,12 @@
"node": "*"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@ -3266,6 +3365,12 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT"
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
@ -3297,7 +3402,6 @@
"resolved": "https://registry.npmjs.org/edge-error/-/edge-error-4.0.2.tgz",
"integrity": "sha512-jB76VYn8wapDHKHSOmP3vbKLoa77RJYsTLNmfl8+cuCD69uxZtP3h+kqV+Prw/YkYmN7yHyp4IApE15pDByk0A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.16.0"
}
@ -3307,7 +3411,6 @@
"resolved": "https://registry.npmjs.org/edge-lexer/-/edge-lexer-6.0.3.tgz",
"integrity": "sha512-/s15CNnfhZv97bsW+ZgV5rtONULYjhCDYu+usbVLqZ8UQ6b/hQUNvQSIQBXA6Gql9dm72TMBB9sb/eWM2esufg==",
"license": "MIT",
"peer": true,
"dependencies": {
"edge-error": "^4.0.2"
},
@ -3320,7 +3423,6 @@
"resolved": "https://registry.npmjs.org/edge-parser/-/edge-parser-9.0.4.tgz",
"integrity": "sha512-vnjzfpqpjM4Mjt9typc1zLoFpC1F6kAObfcdyA6rSy+izIPji2RaQz5jWx5s5iG9hNcuyjtNyGRCLFVfoYhWcA==",
"license": "MIT",
"peer": true,
"dependencies": {
"acorn": "^8.14.0",
"astring": "^1.9.0",
@ -3337,7 +3439,6 @@
"resolved": "https://registry.npmjs.org/edge.js/-/edge.js-6.2.1.tgz",
"integrity": "sha512-me875zh6YA0V429hywgQIpHgMvQkondv5XHaP6EsL2yIBpLcBWCl7Ba1cai0SwYhp8iD0IyV3KjpxLrnW7S2Ag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@poppinss/inspect": "^1.0.1",
"@poppinss/macroable": "^1.0.4",
@ -3510,7 +3611,6 @@
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz",
"integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -4174,8 +4274,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
"integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/function-bind": {
"version": "1.1.2",
@ -4896,8 +4995,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
@ -5300,6 +5398,18 @@
"dev": true,
"license": "ISC"
},
"node_modules/matchit": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/matchit/-/matchit-1.1.0.tgz",
"integrity": "sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==",
"license": "MIT",
"dependencies": {
"@arr/every": "^1.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -6351,7 +6461,6 @@
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
"integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@ -7317,7 +7426,6 @@
"resolved": "https://registry.npmjs.org/stringify-attributes/-/stringify-attributes-4.0.0.tgz",
"integrity": "sha512-6Hq3K153wTTfhEHb4V/viuqmb0DRn08JCrRnmqc4Q/tmoNuvd4DEyqkiiJXtvVz8ZSUhlCQr7zCpCVTgrelesg==",
"license": "MIT",
"peer": true,
"dependencies": {
"escape-goat": "^4.0.0"
},
@ -7835,6 +7943,15 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/validator": {
"version": "13.15.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz",
"integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View file

@ -59,6 +59,10 @@
"@adonisjs/lucid": "^21.6.1",
"@adonisjs/mail": "^9.2.2",
"@adonisjs/redis": "^9.2.0",
"@adonisjs/transmit": "^2.0.2",
"@vinejs/vine": "^3.0.1",
"adonis-captcha-guard": "^1.0.1",
"edge.js": "^6.2.1",
"luxon": "^3.6.1",
"pg": "^8.16.0",
"reflect-metadata": "^0.2.2"

View file

@ -0,0 +1,19 @@
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 :
{{ formattedOTP }}
Ce lien et ce code expireront dans {{ expiresIn }} et ne peuvent être utilisés qu'une seule fois.
Si vous n'avez pas demandé cette connexion, vous pouvez ignorer cet e-mail.
---
Cet e-mail a été envoyé à {{ email }}
© {{ new Date().getFullYear() }} Khollisé. Tous droits réservés.

View file

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ emailTitle }}</title>
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f9f9f9;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);">
<div style="padding: 5px 24px 32px 24px;">
<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 :
</p>
<div style="margin: 24px 0; text-align: center;">
<div style="font-family: monospace; font-size: 24px; letter-spacing: 4px; background-color: #f3f4f6; padding: 12px 16px; border-radius: 4px; font-weight: bold; color: #1f2937; display: inline-block;">
{{ formattedOTP }}
</div>
</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.
</p>
<p style="font-size: 14px; color: #6b7280; font-style: italic; margin-bottom: 16px;">
Si vous n'avez pas demandé cette connexion, vous pouvez ignorer cet email en toute sécurité.
</p>
</div>
<div style="background-color: #f3f4f6; padding: 16px 24px; text-align: center; font-size: 12px; color: #6b7280;">
<p style="margin: 0 0 4px 0;">Cet email a été envoyé à {{ email }}</p>
<p style="margin: 0;">&copy; {{ new Date().getFullYear() }} Khollisé. Tous droits réservés.</p>
</div>
</div>
</body>
</html>

View file

@ -48,8 +48,9 @@ export default await Env.create(new URL('../', import.meta.url), {
| Variables for configuring the mail package
|----------------------------------------------------------
*/
// MAILGUN_API_KEY: Env.schema.string(),
// MAILGUN_DOMAIN: Env.schema.string()
MAILGUN_API_KEY: Env.schema.string(),
MAILGUN_DOMAIN: Env.schema.string(),
MAIL_FROM: Env.schema.string(),
/*
|----------------------------------------------------------
@ -59,4 +60,7 @@ export default await Env.create(new URL('../', import.meta.url), {
REDIS_HOST: Env.schema.string({ format: 'host' }),
REDIS_PORT: Env.schema.number(),
REDIS_PASSWORD: Env.schema.string.optional(),
TURNSTILE_SITE_KEY: Env.schema.string.optional(),
TURNSTILE_SECRET: Env.schema.string.optional(),
})

11
start/limiters.ts Normal file
View file

@ -0,0 +1,11 @@
import limiter from '@adonisjs/limiter/services/main'
/// For global rate limiting (critical routes)
export const throttle = limiter.define('global', () => {
return limiter.allowRequests(10).every('1 minute')
})
// For mail sending
export const authThrottle = limiter.define('auth', () => {
return limiter.allowRequests(3).every('5 minutes')
})

View file

@ -8,5 +8,17 @@
*/
import router from '@adonisjs/core/services/router'
import transmit from '@adonisjs/transmit/services/main'
import { authThrottle } from './limiters.js'
import { throttle } from './limiter.js'
router.get('/', async () => 'It works!')
transmit.registerRoutes()
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.get('/auth/magic-link', 'AuthController.magicLink').use(throttle)
// router.get('/auth/listen', 'AuthController.listen')
})