Skip to content

Commit

Permalink
feat: add verify otp code
Browse files Browse the repository at this point in the history
  • Loading branch information
DeVoresyah committed Dec 24, 2023
1 parent 667324f commit 0885ec5
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 4 deletions.
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 @@ -10,7 +10,7 @@ import ResendService from 'App/Services/ResendService'
import TwilioService from 'App/Services/TwilioService'

// Validators
import RegisterWithPassword from 'App/Validators/v1/Auth/Email/RegisterWithPasswordValidator'
import RegisterWithPassword from 'App/Validators/v1/Auth/RegisterWithPasswordValidator'

// Models
import User from 'App/Models/User'
Expand Down
122 changes: 122 additions & 0 deletions app/Controllers/Http/v1/Auth/VerifiesController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { StatusCodes } from 'http-status-codes'
import Md5 from 'App/Helpers/Md5Helper'
import StringTransform from 'App/Helpers/StringTransform'
import JwtService from 'App/Services/JwtService'
import { DateTime } from 'luxon'
import Database from '@ioc:Adonis/Lucid/Database'
import { cuid } from '@ioc:Adonis/Core/Helpers'

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

// Models
import User from 'App/Models/User'
import Session from 'App/Models/Session'
import RefreshToken from 'App/Models/RefreshToken'
import Identity from 'App/Models/Identity'

// Types
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class VerifiesController {
private md5 = new Md5()
private jwt = new JwtService()

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

if (
(payload.type === 'sms' && !payload.phone) ||
(payload.type === 'whatsapp' && !payload.phone)
) {
return response.api({ message: 'Invalid verification request.' }, StatusCodes.BAD_REQUEST)
}

if (payload.type === 'email' && !payload.email) {
return response.api({ message: 'Invalid verification request.' }, StatusCodes.BAD_REQUEST)
}

const queryUser = User.query()

if (payload.email) {
queryUser.where('email', payload.email)
}

if (payload.phone) {
queryUser.where('phone', payload.phone)
}

const user = await queryUser.first()

if (!user) {
return response.api({ message: 'Invalid verification request.' }, StatusCodes.BAD_REQUEST)
}

// 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)
}

// Validate OTP Code
const validateOtp = await this.md5.verify(payload.otp, user.confirmationToken)

if (!validateOtp) {
return response.api({ message: 'Invalid OTP Code.' }, StatusCodes.UNAUTHORIZED)
}

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.api(userToken, StatusCodes.OK)
} else {
return response.api({ message: 'Internal server error.' }, StatusCodes.INTERNAL_SERVER_ERROR)
}
}
}
10 changes: 8 additions & 2 deletions app/Helpers/Md5Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ export default class Md5 {
}

public verify(plainText: string, hashed: string) {
const hash = this.hasher.update(plainText).digest('hex')
return new Promise((resolve, reject) => {
try {
const hash = this.hasher.update(plainText).digest('hex')

return hash === hashed
resolve(hash === hashed)
} catch (e) {
reject(e)
}
})
}
}
12 changes: 12 additions & 0 deletions app/Helpers/StringTransform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { markdownToTxt } from 'markdown-to-txt'
import { DateTime } from 'luxon'

export default class StringTransform {
public static toSlug(str: string): string {
Expand Down Expand Up @@ -33,4 +34,15 @@ export default class StringTransform {
const otp = Math.floor(Math.random() * 900000) + 100000
return otp.toString()
}

public static isOtpExpired(confirmationSentAt: DateTime<boolean>) {
const sentAt = DateTime.fromISO(confirmationSentAt.toString())
const now = DateTime.now()

// Calculate the difference in minutes
const diffInMinutes = now.diff(sentAt, 'minutes').minutes

// Check if the difference is more than 60 minutes
return diffInMinutes > 60
}
}
5 changes: 5 additions & 0 deletions app/Models/RefreshToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
import type { DateTime } from 'luxon'

export default class RefreshToken extends BaseModel {
public static table = 'auth.refresh_tokens'

@column({ isPrimary: true })
public id: string

Expand All @@ -19,6 +21,9 @@ export default class RefreshToken extends BaseModel {
@column()
public revoked: boolean

@column()
public parent: string | null

@column.dateTime({ autoCreate: true })
public createdAt: DateTime

Expand Down
2 changes: 2 additions & 0 deletions app/Models/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
import type { DateTime } from 'luxon'

export default class Session extends BaseModel {
public static table = 'auth.sessions'

@column({ isPrimary: true })
public id: string

Expand Down
2 changes: 1 addition & 1 deletion app/Types/authentication.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
interface Token {
user_id: number
user_id: string
}

interface JwtGeneratePayload {
Expand Down
19 changes: 19 additions & 0 deletions app/Validators/v1/Verify/VerifyOtpValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { schema, rules, CustomMessages } from '@ioc:Adonis/Core/Validator'
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

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

public schema = schema.create({
phone: schema.string.optional(),
email: schema.string.optional({}, [rules.email()]),
otp: schema.string(),
type: schema.enum(['sms', 'whatsapp', 'email']),
})

public messages: CustomMessages = {
required: '{{ field }} cannot be empty.',
unique: '{{ field }} already exists.',
email: '{{ field }} is not a valid email.',
}
}
1 change: 1 addition & 0 deletions database/migrations/1702737445458_refresh_tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default class extends BaseSchema {
table.uuid('session_id').references('id').inTable('auth.sessions')
table.string('token')
table.boolean('revoked').defaultTo(false)
table.string('parent').nullable()
table.timestamp('created_at', { useTz: true })
table.timestamp('updated_at', { useTz: true })
})
Expand Down
1 change: 1 addition & 0 deletions routes/auth/v1/index.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('/register', 'AuthsController.signUpWithPassword')
require('./verify')
})
.prefix('/v1')
.namespace('App/Controllers/Http/v1/Auth')
7 changes: 7 additions & 0 deletions routes/auth/v1/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Route from '@ioc:Adonis/Core/Route'

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

0 comments on commit 0885ec5

Please sign in to comment.