Skip to content

Commit d3e747d

Browse files
committed
feat: auth
1 parent 24ab6ec commit d3e747d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1614
-27
lines changed

.env.example

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ PORT=3333
33
HOST=0.0.0.0
44
LOG_LEVEL=info
55
APP_KEY=
6+
BASE_URL=http://localhost:3333
67
NODE_ENV=development
78
SESSION_DRIVER=cookie
89
DB_HOST=127.0.0.1
910
DB_PORT=3306
1011
DB_USER=root
1112
DB_PASSWORD=root
12-
DB_DATABASE=app
13+
DB_DATABASE=app
14+
SMTP_HOST=localhost
15+
SMTP_PORT=1025
16+
SMTP_USERNAME=username
17+
SMTP_PASSWORD=password
18+
SMTP_SECURE=false
19+
SMTP_FROM_EMAIL=dev@example.fr
20+
SMTP_FROM_NAME="Dev Aquatracking"

.vscode/extensions.json

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"recommendations": [
33
"jripouteau.adonis-extension-pack",
4-
"vue.volar",
54
"bradlc.vscode-tailwindcss",
65
"csstools.postcss",
76
"lokalise.i18n-ally"

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"editor.formatOnSave": true,
33
"editor.defaultFormatter": "esbenp.prettier-vscode",
44
"i18n-ally.enabledFrameworks": ["vue"],
5-
"i18n-ally.localesPaths": ["resources/lang"]
5+
"i18n-ally.localesPaths": ["resources/lang"],
6+
"i18n-ally.keystyle": "nested"
67
}

adonisrc.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ export default defineConfig({
1010
| will be scanned automatically from the "./commands" directory.
1111
|
1212
*/
13-
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands')],
13+
commands: [
14+
() => import('@adonisjs/core/commands'),
15+
() => import('@adonisjs/lucid/commands'),
16+
() => import('@adonisjs/mail/commands'),
17+
],
1418

1519
/*
1620
|--------------------------------------------------------------------------
@@ -39,6 +43,7 @@ export default defineConfig({
3943
() => import('@adonisjs/auth/auth_provider'),
4044
() => import('@adonisjs/inertia/inertia_provider'),
4145
() => import('@adonisjs/i18n/i18n_provider'),
46+
() => import('@adonisjs/mail/mail_provider'),
4247
],
4348

4449
/*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import UserToken from '#models/user_token'
2+
import type { HttpContext } from '@adonisjs/core/http'
3+
import vine from '@vinejs/vine'
4+
5+
export default class EmailVerificationController {
6+
async verify({ request, session, response, i18n }: HttpContext) {
7+
const { token } = request.qs()
8+
9+
const validatedToken = await vine
10+
.compile(vine.string())
11+
.validate(token)
12+
.catch(() => null)
13+
14+
if (!validatedToken) {
15+
session.flashErrors({
16+
E_MISSING_EMAIL_VERIFICATION_TOKEN: i18n.t('errors.E_MISSING_EMAIL_VERIFICATION_TOKEN'),
17+
})
18+
return response.redirect().toPath('/auth/login')
19+
}
20+
21+
const user = await UserToken.getEmailVerificationUser(validatedToken)
22+
23+
if (user) {
24+
user.verified = true
25+
await user.save()
26+
27+
await UserToken.deleteUserEmailVerificationTokens(user)
28+
29+
session.flash('notification', {
30+
type: 'success',
31+
message: i18n.t('notifications.emailVerified'),
32+
})
33+
} else {
34+
session.flashErrors({
35+
E_INVALID_EMAIL_VERIFICATION_TOKEN_TRY_AGAIN: i18n.t(
36+
'errors.E_INVALID_EMAIL_VERIFICATION_TOKEN_TRY_AGAIN'
37+
),
38+
})
39+
}
40+
return response.redirect().toPath('/auth/login')
41+
}
42+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import User from '#models/user'
2+
import UserToken from '#models/user_token'
3+
import MailService from '#services/mail_service'
4+
import { loginValidator } from '#validators/login_validator'
5+
import { inject } from '@adonisjs/core'
6+
import type { HttpContext } from '@adonisjs/core/http'
7+
8+
@inject()
9+
export default class LoginController {
10+
constructor(protected mailService: MailService) {}
11+
12+
/**
13+
* Display the login form
14+
*/
15+
async index({ inertia }: HttpContext) {
16+
return inertia.render('auth/login')
17+
}
18+
19+
/**
20+
* Handle form submission for the login action
21+
*/
22+
async store({ request, auth, response, session, i18n }: HttpContext) {
23+
const { email, password, rememberMe } = await request.validateUsing(loginValidator)
24+
25+
const user = await User.verifyCredentials(email, password)
26+
27+
if (!user.verified) {
28+
const token = await UserToken.generateEmailVerificationToken(user)
29+
await this.mailService.sendEmailVerification(user, token, i18n.locale)
30+
31+
session.flash('notification', {
32+
type: 'warn',
33+
message: i18n.t('notifications.emailNotVerified'),
34+
})
35+
36+
return response.redirect().back()
37+
}
38+
39+
await auth.use('web').login(user, rememberMe)
40+
41+
return response.redirect('/')
42+
}
43+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { HttpContext } from '@adonisjs/core/http'
2+
3+
export default class LogoutController {
4+
/**
5+
* Handle form submission for the logout action
6+
*/
7+
async store({ auth, response }: HttpContext) {
8+
await auth.use('web').logout()
9+
10+
return response.redirect('/auth/login')
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import User from '#models/user'
2+
import UserToken from '#models/user_token'
3+
import MailService from '#services/mail_service'
4+
import { passwordForgotValidator } from '#validators/password_forgot_validator'
5+
import { passwordResetValidator } from '#validators/password_reset_validator'
6+
import { inject } from '@adonisjs/core'
7+
import { type HttpContext } from '@adonisjs/core/http'
8+
9+
@inject()
10+
export default class PasswordResetController {
11+
constructor(protected mailService: MailService) {}
12+
13+
async forgot({ inertia }: HttpContext) {
14+
return inertia.render('auth/password/forgot')
15+
}
16+
17+
async send({ request, response, session, i18n }: HttpContext) {
18+
const { email } = await request.validateUsing(passwordForgotValidator)
19+
20+
const user = await User.findBy('email', email)
21+
22+
if (user) {
23+
const token = await UserToken.generatePasswordResetToken(user)
24+
25+
await this.mailService.sendPasswordReset(user, token, i18n.locale)
26+
}
27+
28+
session.flash('notification', {
29+
type: 'success',
30+
message: i18n.t('notifications.resetLinkSent'),
31+
})
32+
33+
return response.redirect().back()
34+
}
35+
36+
async reset({ inertia, request }: HttpContext) {
37+
return inertia.render('auth/password/reset', {
38+
token: request.qs().token,
39+
})
40+
}
41+
42+
async store({ request, response, session, i18n }: HttpContext) {
43+
const { token, password } = await request.validateUsing(passwordResetValidator)
44+
45+
const user = await UserToken.getPasswordResetUser(token)
46+
47+
if (!user) {
48+
session.flash('errors', {
49+
token: [i18n.t('validation.invalidToken')],
50+
})
51+
session.flashAll()
52+
return response.redirect().back()
53+
}
54+
55+
user.password = password
56+
await user.save()
57+
58+
await UserToken.deleteUserPasswordResetTokens(user)
59+
60+
await this.mailService.sendPasswordResetConfirmation(user, i18n.locale)
61+
62+
session.flash('notification', {
63+
type: 'success',
64+
message: i18n.t('notifications.passwordReset'),
65+
})
66+
67+
return response.redirect().toPath('/auth/login')
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import User from '#models/user'
2+
import UserToken from '#models/user_token'
3+
import MailService from '#services/mail_service'
4+
import { createRegisterValidator } from '#validators/register_validator'
5+
import { inject } from '@adonisjs/core'
6+
import type { HttpContext } from '@adonisjs/core/http'
7+
8+
@inject()
9+
export default class RegisterController {
10+
constructor(protected mailService: MailService) {}
11+
12+
/**
13+
* Display the registration form
14+
*/
15+
async index({ inertia }: HttpContext) {
16+
return inertia.render('auth/register')
17+
}
18+
19+
/**
20+
* Handle form submission for the create action
21+
*/
22+
async store({ request, response, session, i18n }: HttpContext) {
23+
const { fullName, email, password } = await request.validateUsing(createRegisterValidator)
24+
25+
const user = await User.create({ fullName, email, password })
26+
27+
const token = await UserToken.generateEmailVerificationToken(user)
28+
await this.mailService.sendEmailVerification(user, token, i18n.locale)
29+
30+
session.flash('notification', {
31+
type: 'success',
32+
message: i18n.t('notifications.accountCreatedCheckEmail'),
33+
})
34+
35+
return response.redirect().toPath('/auth/login')
36+
}
37+
}

app/controllers/home_controller.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { HttpContext } from '@adonisjs/core/http'
2+
3+
export default class HomeController {
4+
async index({ inertia }: HttpContext) {
5+
return inertia.render('home')
6+
}
7+
}

app/dto/user_dto.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import vine from '@vinejs/vine'
2+
import { Infer } from '@vinejs/vine/types'
3+
4+
const userDtoSchema = vine.object({
5+
id: vine.string(),
6+
fullName: vine.string(),
7+
email: vine.string(),
8+
verified: vine.boolean(),
9+
createdAt: vine.date(),
10+
updatedAt: vine.date(),
11+
})
12+
13+
export const userDtoValidator = vine.compile(userDtoSchema)
14+
15+
export type UserDto = Infer<typeof userDtoSchema>

app/middleware/auth_middleware.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default class AuthMiddleware {
1010
/**
1111
* The URL to redirect to, when authentication fails
1212
*/
13-
redirectTo = '/login'
13+
redirectTo = '/auth/login'
1414

1515
async handle(
1616
ctx: HttpContext,

app/models/user.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
import { DateTime } from 'luxon'
2-
import hash from '@adonisjs/core/services/hash'
3-
import { compose } from '@adonisjs/core/helpers'
4-
import { BaseModel, column } from '@adonisjs/lucid/orm'
51
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
2+
import { DbRememberMeTokensProvider } from '@adonisjs/auth/session'
3+
import { compose } from '@adonisjs/core/helpers'
4+
import hash from '@adonisjs/core/services/hash'
5+
import { BaseModel, beforeCreate, column, hasMany } from '@adonisjs/lucid/orm'
6+
import type { HasMany } from '@adonisjs/lucid/types/relations'
7+
import { DateTime } from 'luxon'
8+
import { randomUUID } from 'node:crypto'
9+
import UserToken from './user_token.js'
610

711
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
812
uids: ['email'],
913
passwordColumnName: 'password',
1014
})
1115

1216
export default class User extends compose(BaseModel, AuthFinder) {
17+
static selfAssignPrimaryKey = true
18+
1319
@column({ isPrimary: true })
14-
declare id: number
20+
declare id: string
1521

1622
@column()
1723
declare fullName: string | null
@@ -22,9 +28,32 @@ export default class User extends compose(BaseModel, AuthFinder) {
2228
@column({ serializeAs: null })
2329
declare password: string
2430

31+
@column()
32+
declare verified: boolean
33+
2534
@column.dateTime({ autoCreate: true })
2635
declare createdAt: DateTime
2736

2837
@column.dateTime({ autoCreate: true, autoUpdate: true })
2938
declare updatedAt: DateTime | null
39+
40+
@hasMany(() => UserToken)
41+
declare tokens: HasMany<typeof UserToken>
42+
43+
@hasMany(() => UserToken, {
44+
onQuery: (query) => query.where('type', 'PASSWORD_RESET'),
45+
})
46+
declare passwordResetToken: HasMany<typeof UserToken>
47+
48+
@hasMany(() => UserToken, {
49+
onQuery: (query) => query.where('type', 'EMAIL_VERIFICATION'),
50+
})
51+
declare emailVerificationToken: HasMany<typeof UserToken>
52+
53+
static readonly rememberMeTokens = DbRememberMeTokensProvider.forModel(User)
54+
55+
@beforeCreate()
56+
static assignUuid(user: User) {
57+
user.id = randomUUID()
58+
}
3059
}

0 commit comments

Comments
 (0)