Skip to content

Commit

Permalink
feat: add verification magic link
Browse files Browse the repository at this point in the history
  • Loading branch information
DeVoresyah committed Dec 24, 2023
1 parent 0885ec5 commit 779a606
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ HOST=0.0.0.0
NODE_ENV=development
APP_NAME=Sokudo
APP_KEY=Iw3tQN8WBEPVf4Ep8z45e_eaSOYZcSPj
APP_URL=http://localhost:3000
APP_URL=http://localhost:3333
DRIVE_DISK=local
DB_CONNECTION=pg
PG_HOST=localhost
Expand Down
2 changes: 1 addition & 1 deletion app/Controllers/Http/v1/Auth/AuthsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default class AuthsController {

if (result.user && result.identity) {
if (payload.email) {
this.mailer.sendEmailVerification(payload.email, confirmationToken, 'http://localhost:3000')
this.mailer.sendEmailVerification(payload.email, confirmationToken, 'http://localhost:3333')
} else {
if (payload.channel === 'whatsapp') {
this.twilio.sendOtpWhatsapp(otpCode, payload.phone!)
Expand Down
94 changes: 93 additions & 1 deletion app/Controllers/Http/v1/Auth/VerifiesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { cuid } from '@ioc:Adonis/Core/Helpers'

// Validators
import VerifyOtpValidator from 'App/Validators/v1/Verify/VerifyOtpValidator'
import VerifyMagicLinkValidator from 'App/Validators/v1/Verify/VerifyMagicLinkValidator'

// Models
import User from 'App/Models/User'
Expand Down Expand Up @@ -56,7 +57,7 @@ export default class VerifiesController {
// Check if session is invalid
// OTP Code should valid for 60 mins
if (StringTransform.isOtpExpired(user.confirmationSentAt)) {
return response.api({ message: 'Invalid OTP Code.' }, StatusCodes.UNAUTHORIZED)
return response.api({ message: 'OTP Code is expired.' }, StatusCodes.UNAUTHORIZED)
}

// Validate OTP Code
Expand Down Expand Up @@ -119,4 +120,95 @@ export default class VerifiesController {
return response.api({ message: 'Internal server error.' }, StatusCodes.INTERNAL_SERVER_ERROR)
}
}

public async verifyMagicLink({ request, response }: HttpContextContract) {
const payload = await request.validate(VerifyMagicLinkValidator)
const headers = request.headers()

const user = await User.findBy('confirmation_token', payload.token)

if (!user) {
return response
.redirect()
.withQs({ message: 'Invalid verification request', status: 'failed' })
.toPath(payload.redirect)
}

// Check if session is invalid
// OTP Code should valid for 60 mins
if (StringTransform.isOtpExpired(user.confirmationSentAt)) {
return response
.redirect()
.withQs({
message: 'Verification request is expired',
status: 'failed',
})
.toPath(payload.redirect)
}

const newSession = await Database.transaction(async (trx) => {
user.useTransaction(trx)

const lastSignedAt = DateTime.now()

user.lastSignInAt = lastSignedAt
await user.save()

const identity = await Identity.query({ client: trx })
.where('user_id', user.id)
.andWhere(
'provider',
payload.type === 'sms' || payload.type === 'whatsapp' ? 'phone' : 'email'
)
.first()

identity!.lastSignInAt = lastSignedAt
await identity?.save()

const session = await Session.create(
{
userId: user.id,
userAgent: headers['user-agent'],
ip: request.ips()[0],
},
{ client: trx }
)

const refreshToken = await RefreshToken.create(
{
userId: user.id,
sessionId: session.id,
token: cuid(),
revoked: false,
parent: null,
},
{ client: trx }
)

return {
session,
refreshToken,
}
})

if (newSession.session && newSession.refreshToken) {
const userToken = this.jwt.generate({ user_id: user.id }).make()

return response
.redirect()
.withQs({
token: userToken.token,
status: 'success',
})
.toPath(payload.redirect)
} else {
return response
.redirect()
.withQs({
message: 'Internal server error',
status: 'failed',
})
.toPath(payload.redirect)
}
}
}
27 changes: 27 additions & 0 deletions app/Validators/v1/Verify/VerifyMagicLinkValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { schema, CustomMessages } from '@ioc:Adonis/Core/Validator'
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class VerifyMagicLinkValidator {
constructor(protected ctx: HttpContextContract) {}

public schema = schema.create({
token: schema.string(),
type: schema.string(),
redirect: schema.string(),
})

/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {
required: '{{ field }} cannot be empty.',
}
}
1 change: 1 addition & 0 deletions routes/auth/v1/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Route from '@ioc:Adonis/Core/Route'

Route.group(() => {
Route.post('/otp', 'VerifiesController.verifyOtp')
Route.get('/', 'VerifiesController.verifyMagicLink')
})
.prefix('/verify')
.namespace('App/Controllers/Http/v1/Auth')

0 comments on commit 779a606

Please sign in to comment.