✨ Introduce auth flow
This commit is contained in:
parent
211e6e26f5
commit
73db24d3c0
11 changed files with 244 additions and 38 deletions
|
|
@ -23,4 +23,6 @@ MAILGUN_DOMAIN=
|
||||||
TURNSTILE_SITE_KEY=
|
TURNSTILE_SITE_KEY=
|
||||||
TURNSTILE_SECRET=
|
TURNSTILE_SECRET=
|
||||||
|
|
||||||
PUBLIC_AUTH_URL=
|
PUBLIC_AUTH_URL=
|
||||||
|
|
||||||
|
SESSION_DRIVER=cookie
|
||||||
20
adonisrc.ts
20
adonisrc.ts
|
|
@ -25,7 +25,12 @@ export default defineConfig({
|
||||||
| will be scanned automatically from the "./commands" directory.
|
| will be scanned automatically from the "./commands" directory.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands'), () => import('@adonisjs/cache/commands'), () => import('@adonisjs/mail/commands')],
|
commands: [
|
||||||
|
() => import('@adonisjs/core/commands'),
|
||||||
|
() => import('@adonisjs/lucid/commands'),
|
||||||
|
() => import('@adonisjs/cache/commands'),
|
||||||
|
() => import('@adonisjs/mail/commands'),
|
||||||
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -54,7 +59,8 @@ export default defineConfig({
|
||||||
() => import('@adonisjs/transmit/transmit_provider'),
|
() => import('@adonisjs/transmit/transmit_provider'),
|
||||||
() => import('@adonisjs/core/providers/vinejs_provider'),
|
() => import('@adonisjs/core/providers/vinejs_provider'),
|
||||||
() => import('adonis-captcha-guard/providers/captcha_provider'),
|
() => import('adonis-captcha-guard/providers/captcha_provider'),
|
||||||
() => import('@adonisjs/core/providers/edge_provider')
|
() => import('@adonisjs/core/providers/edge_provider'),
|
||||||
|
() => import('@adonisjs/session/session_provider'),
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -91,8 +97,10 @@ export default defineConfig({
|
||||||
],
|
],
|
||||||
forceExit: false,
|
forceExit: false,
|
||||||
},
|
},
|
||||||
metaFiles: [{
|
metaFiles: [
|
||||||
pattern: 'resources/views/**/*.edge',
|
{
|
||||||
reloadServer: false,
|
pattern: 'resources/views/**/*.edge',
|
||||||
}]
|
reloadServer: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import { AuthService } from '#services/auth_service'
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core'
|
||||||
import app from '@adonisjs/core/services/app'
|
import app from '@adonisjs/core/services/app'
|
||||||
import env from '#start/env'
|
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()
|
@inject()
|
||||||
export default class AuthController {
|
export default class AuthController {
|
||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
@ -23,20 +25,33 @@ export default class AuthController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Generate token
|
||||||
const expiresIn = '15 minutes'
|
const expiresIn = '15 minutes'
|
||||||
const { email } = await request.validateUsing(requestLoginValidator)
|
const payload = await this.authService.generateToken(email, user.id, expiresIn)
|
||||||
const payload = await this.authService.generateToken(email, expiresIn)
|
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
await mail.send((message) => {
|
await mail
|
||||||
message
|
.send((message) => {
|
||||||
.from(env.get('MAIL_FROM')!)
|
message
|
||||||
.to(email)
|
.from(env.get('MAIL_FROM')!)
|
||||||
.subject(payload.emailTitle)
|
.to(email)
|
||||||
.htmlView('mails/auth', payload)
|
.subject(payload.emailTitle)
|
||||||
.textView('mails/auth-fallback', payload)
|
.htmlView('mails/auth', payload)
|
||||||
}).then(console.log).catch(console.error)
|
.textView('mails/auth-fallback', payload)
|
||||||
|
})
|
||||||
|
// TODO: Handle email sending errors
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.error)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -47,19 +62,33 @@ export default class AuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /auth/verify
|
// POST /auth/verify
|
||||||
async verifyCode({ request }: HttpContext) {
|
async verifyCode({ request, response, auth }: HttpContext) {
|
||||||
// Validate code
|
// Validate code
|
||||||
const { code } = await request.validateUsing(verifyCodeValidator)
|
const { code } = await request.validateUsing(verifyCodeValidator)
|
||||||
const email = await this.authService.validateCode(code)
|
const { success, userId, email } = await this.authService.validateCode(code)
|
||||||
if (!email) {
|
if (!success || !userId || isNaN(userId)) {
|
||||||
return {
|
return response.badRequest({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Le code est invalide ou a expiré',
|
message: 'Code de vérification invalide',
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOOD: Login
|
// Find user by id
|
||||||
// Find user by email (string similary)
|
const user = await User.findBy('id', userId)
|
||||||
|
if (!user) {
|
||||||
|
return response.notFound({
|
||||||
|
success: false,
|
||||||
|
message: 'Utilisateur non trouvé',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
magicLink({}: HttpContext) {
|
magicLink({}: HttpContext) {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ import User from '#models/user'
|
||||||
import { createUserValidator } from '#validators/user'
|
import { createUserValidator } from '#validators/user'
|
||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
import { cuid } from '@adonisjs/core/helpers'
|
import { cuid } from '@adonisjs/core/helpers'
|
||||||
import drive from '@adonisjs/drive/services/main'
|
|
||||||
|
|
||||||
export default class UserController {
|
export default class UserController {
|
||||||
// GET /user/@me
|
// GET /users/@me
|
||||||
async me({ auth }: HttpContext) {
|
async me({ auth }: HttpContext) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -13,14 +12,13 @@ export default class UserController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /user
|
// POST /users
|
||||||
async create({ request }: HttpContext) {
|
async create({ request }: HttpContext) {
|
||||||
const payload = await request.validateUsing(createUserValidator)
|
const payload = await request.validateUsing(createUserValidator)
|
||||||
// Save avatar
|
// Save avatar
|
||||||
const avatar = `avatars/${cuid()}.${payload.avatar.extname}`
|
const avatar = `avatars/${cuid()}.${payload.avatar.extname}`
|
||||||
await payload.avatar.moveToDisk(avatar)
|
await payload.avatar.moveToDisk(avatar)
|
||||||
// const avatar = await drive.use().getSignedUrl(key)
|
// const avatar = await drive.use().getSignedUrl(key)
|
||||||
console.log(avatar)
|
|
||||||
return User.create({
|
return User.create({
|
||||||
...payload,
|
...payload,
|
||||||
avatar,
|
avatar,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
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, expiresIn: string) {
|
async generateToken(email: string, userId: number, expiresIn: string) {
|
||||||
// Generate magic link token
|
// Generate magic link token
|
||||||
const token = encryption.encrypt(email, expiresIn)
|
const identifier = `${email}:${userId}`
|
||||||
|
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
|
||||||
|
|
@ -14,7 +19,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: email,
|
value: identifier,
|
||||||
ttl: expiresIn,
|
ttl: expiresIn,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -29,15 +34,74 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 email = await cache.get({ key })
|
const id = await cache.get({ key })
|
||||||
if (!email) return false
|
if (!id) return { success: false, userId: null, email: null }
|
||||||
|
|
||||||
// Delete code from cache
|
// Delete code from cache
|
||||||
await cache.delete({ key })
|
await cache.delete({ key })
|
||||||
|
|
||||||
return email
|
return {
|
||||||
|
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'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
findUserByEmail(email: string) {
|
||||||
|
return User.query().where('email', email).first()
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUser(email: string) {
|
||||||
|
// Try to find user by email
|
||||||
|
let user: User | null | undefined = await this.findUserByEmail(email)
|
||||||
|
if (user) return user
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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}"`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
config/session.ts
Normal file
48
config/session.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import env from '#start/env'
|
||||||
|
import app from '@adonisjs/core/services/app'
|
||||||
|
import { defineConfig, stores } from '@adonisjs/session'
|
||||||
|
|
||||||
|
const sessionConfig = defineConfig({
|
||||||
|
enabled: true,
|
||||||
|
cookieName: 'adonis-session',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When set to true, the session id cookie will be deleted
|
||||||
|
* once the user closes the browser.
|
||||||
|
*/
|
||||||
|
clearWithBrowser: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define how long to keep the session data alive without
|
||||||
|
* any activity.
|
||||||
|
*/
|
||||||
|
age: '2h',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for session cookie and the
|
||||||
|
* cookie store
|
||||||
|
*/
|
||||||
|
cookie: {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: false,
|
||||||
|
secure: app.inProduction,
|
||||||
|
sameSite: 'lax',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The store to use. Make sure to validate the environment
|
||||||
|
* variable in order to infer the store name without any
|
||||||
|
* errors.
|
||||||
|
*/
|
||||||
|
store: env.get('SESSION_DRIVER'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of configured stores. Refer documentation to see
|
||||||
|
* list of available stores and their config.
|
||||||
|
*/
|
||||||
|
stores: {
|
||||||
|
cookie: stores.cookie(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default sessionConfig
|
||||||
53
package-lock.json
generated
53
package-lock.json
generated
|
|
@ -18,9 +18,11 @@
|
||||||
"@adonisjs/lucid": "^21.6.1",
|
"@adonisjs/lucid": "^21.6.1",
|
||||||
"@adonisjs/mail": "^9.2.2",
|
"@adonisjs/mail": "^9.2.2",
|
||||||
"@adonisjs/redis": "^9.2.0",
|
"@adonisjs/redis": "^9.2.0",
|
||||||
|
"@adonisjs/session": "^7.5.1",
|
||||||
"@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",
|
||||||
|
|
@ -698,6 +700,48 @@
|
||||||
"node": ">=18.16.0"
|
"node": ">=18.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@adonisjs/session": {
|
||||||
|
"version": "7.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@adonisjs/session/-/session-7.5.1.tgz",
|
||||||
|
"integrity": "sha512-b1E0W/1nnJfAq3Gv8yPywgsxJ7uzzOBJxxulonXI4t1eSdvJzZGNrFScfVLOcjTwlxwrEFA847tULIQxgR4Spw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@poppinss/macroable": "^1.0.4",
|
||||||
|
"@poppinss/utils": "^6.9.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.16.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@adonisjs/core": "^6.6.0",
|
||||||
|
"@adonisjs/redis": "^8.0.1 || ^9.0.0",
|
||||||
|
"@aws-sdk/client-dynamodb": "^3.658.0",
|
||||||
|
"@aws-sdk/util-dynamodb": "^3.658.0",
|
||||||
|
"@japa/api-client": "^2.0.3 || ^3.0.0",
|
||||||
|
"@japa/browser-client": "^2.0.3",
|
||||||
|
"edge.js": "^6.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@adonisjs/redis": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@aws-sdk/client-dynamodb": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@aws-sdk/util-dynamodb": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@japa/api-client": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@japa/browser-client": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"edge.js": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@adonisjs/transmit": {
|
"node_modules/@adonisjs/transmit": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@adonisjs/transmit/-/transmit-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@adonisjs/transmit/-/transmit-2.0.2.tgz",
|
||||||
|
|
@ -3019,6 +3063,15 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cmpstr": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cmpstr/-/cmpstr-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-0gj7U07fS8XaAx4IY854d6xpttqxVKUtaeI/giOguqDrXD9OQaBlW321NwhoE+WCF7rv+Pj55YL5kEW3jVuxEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/code-block-writer": {
|
"node_modules/code-block-writer": {
|
||||||
"version": "13.0.3",
|
"version": "13.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,11 @@
|
||||||
"@adonisjs/lucid": "^21.6.1",
|
"@adonisjs/lucid": "^21.6.1",
|
||||||
"@adonisjs/mail": "^9.2.2",
|
"@adonisjs/mail": "^9.2.2",
|
||||||
"@adonisjs/redis": "^9.2.0",
|
"@adonisjs/redis": "^9.2.0",
|
||||||
|
"@adonisjs/session": "^7.5.1",
|
||||||
"@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",
|
||||||
|
|
|
||||||
|
|
@ -63,4 +63,6 @@ export default await Env.create(new URL('../', import.meta.url), {
|
||||||
|
|
||||||
TURNSTILE_SITE_KEY: Env.schema.string.optional(),
|
TURNSTILE_SITE_KEY: Env.schema.string.optional(),
|
||||||
TURNSTILE_SECRET: Env.schema.string.optional(),
|
TURNSTILE_SECRET: Env.schema.string.optional(),
|
||||||
|
|
||||||
|
SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ server.use([() => import('#middleware/container_bindings_middleware'), () => imp
|
||||||
* The router middleware stack runs middleware on all the HTTP
|
* The router middleware stack runs middleware on all the HTTP
|
||||||
* requests with a registered route.
|
* requests with a registered route.
|
||||||
*/
|
*/
|
||||||
router.use([() => import('@adonisjs/core/bodyparser_middleware'), () => import('@adonisjs/auth/initialize_auth_middleware')])
|
router.use([() => import('@adonisjs/core/bodyparser_middleware'), () => import('@adonisjs/auth/initialize_auth_middleware'), () => import('@adonisjs/session/session_middleware')])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Named middleware collection must be explicitly assigned to
|
* Named middleware collection must be explicitly assigned to
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,6 @@ router.group(() => {
|
||||||
const UserController = () => import('#controllers/user_controller')
|
const UserController = () => import('#controllers/user_controller')
|
||||||
|
|
||||||
if (app.inDev) {
|
if (app.inDev) {
|
||||||
router.post('user', [UserController, 'create'])
|
router.post('users', [UserController, 'create'])
|
||||||
}
|
}
|
||||||
router.get('/user/@me', [UserController, 'me']).use(middleware.auth())
|
router.get('/users/@me', [UserController, 'me']).use(middleware.auth())
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue