From 12f8716b90b468c83e4a0dc9b426a830a74d6eb7 Mon Sep 17 00:00:00 2001 From: ribeirogab Date: Fri, 20 Sep 2024 19:22:21 -0300 Subject: [PATCH] feat: add dynamo gsi and implement repositories + fixes --- infra/lambda/main.tf | 12 ++ src/configs/dynamo.config.ts | 5 + src/controllers/password.controller.ts | 37 ++++- src/interfaces/models/verification-code.ts | 11 +- .../verification-code.repository.ts | 15 +- src/repositories/session.repository.ts | 80 ++++++++--- src/repositories/user.repository.ts | 133 +++++++++++++++--- .../verification-code.repository.ts | 70 +++++---- src/routers/auth.router.ts | 14 +- .../recovery-password-verify.service.ts | 2 +- src/services/recovery-password.service.ts | 8 +- src/services/refresh-login.service.ts | 39 +++-- src/services/registration-confirm.service.ts | 17 ++- src/services/registration.service.ts | 12 +- src/services/reset-password.service.ts | 24 +++- 15 files changed, 371 insertions(+), 108 deletions(-) diff --git a/infra/lambda/main.tf b/infra/lambda/main.tf index 62bfe53..cacffde 100644 --- a/infra/lambda/main.tf +++ b/infra/lambda/main.tf @@ -19,11 +19,23 @@ resource "aws_dynamodb_table" "auth_resource_table" { type = "S" } + attribute { + name = "ReferenceId" + type = "S" + } + ttl { attribute_name = "TTL" enabled = true } + global_secondary_index { + name = "ReferenceIdIndex" + hash_key = "PK" + range_key = "ReferenceId" + projection_type = "ALL" + } + tags = { Environment = var.environment } diff --git a/src/configs/dynamo.config.ts b/src/configs/dynamo.config.ts index 258f606..dd0eb21 100644 --- a/src/configs/dynamo.config.ts +++ b/src/configs/dynamo.config.ts @@ -5,6 +5,11 @@ import type { EnvConfig } from './env.config'; export enum DynamoPartitionKeysEnum { VerificationCode = 'verification-code', + User = 'user', +} + +export enum DynamoGSIEnum { + ReferenceIdIndex = 'ReferenceIdIndex', } @injectable() diff --git a/src/controllers/password.controller.ts b/src/controllers/password.controller.ts index 1738ddf..6f80897 100644 --- a/src/controllers/password.controller.ts +++ b/src/controllers/password.controller.ts @@ -1,19 +1,48 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; -import { injectable } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; import { HttpStatusCodesEnum } from '@/constants'; +import type { + RecoveryPasswordService, + RecoveryPasswordVerifyService, + ResetPasswordService, + ResetPasswordServiceDto, +} from '@/interfaces'; @injectable() export class PasswordController { - public recovery(_request: FastifyRequest, reply: FastifyReply) { + constructor( + @inject('RecoveryPasswordVerifyService') + private readonly recoveryPasswordVerifyService: RecoveryPasswordVerifyService, + + @inject('RecoveryPasswordService') + private readonly recoveryPasswordService: RecoveryPasswordService, + + @inject('ResetPasswordService') + private readonly resetPasswordService: ResetPasswordService, + ) {} + + public async recovery(request: FastifyRequest, reply: FastifyReply) { + const { email } = request.body as { email: string }; + + await this.recoveryPasswordService.execute({ email }); + return reply.code(HttpStatusCodesEnum.NO_CONTENT).send(); } - public recoveryVerify(_request: FastifyRequest, reply: FastifyReply) { + public async recoveryVerify(request: FastifyRequest, reply: FastifyReply) { + const { code } = request.body as { code: string }; + + await this.recoveryPasswordVerifyService.execute({ code }); + return reply.code(HttpStatusCodesEnum.NO_CONTENT).send(); } - public reset(_request: FastifyRequest, reply: FastifyReply) { + public async reset(request: FastifyRequest, reply: FastifyReply) { + const { code, email, password } = request.body as ResetPasswordServiceDto; + + await this.resetPasswordService.execute({ code, email, password }); + return reply.code(HttpStatusCodesEnum.NO_CONTENT).send(); } } diff --git a/src/interfaces/models/verification-code.ts b/src/interfaces/models/verification-code.ts index 6419c46..b5e3873 100644 --- a/src/interfaces/models/verification-code.ts +++ b/src/interfaces/models/verification-code.ts @@ -3,9 +3,14 @@ export enum VerificationCodeTypeEnum { Registration = 'registration', } +export enum VerificationCodeReservedFieldEnum { + code_expires_at = 'code_expires_at', + code_type = 'code_type', + code = 'code', +} + export type VerificationCode = { - data: Record; - type: VerificationCodeTypeEnum; - expires_at: string; + code_type: VerificationCodeTypeEnum; + code_expires_at: string; code: string; }; diff --git a/src/interfaces/repositories/verification-code.repository.ts b/src/interfaces/repositories/verification-code.repository.ts index bf71fe7..b476691 100644 --- a/src/interfaces/repositories/verification-code.repository.ts +++ b/src/interfaces/repositories/verification-code.repository.ts @@ -4,17 +4,24 @@ import type { } from '../models/verification-code'; export type VerificationCodeRepositoryFilterDto = { - type: VerificationCodeTypeEnum; + code_type: VerificationCodeTypeEnum; code: string; }; +export type VerificationCodeRepositoryCreateDto = Omit< + VerificationCode, + 'code' +> & { + content: Record; +}; + export type VerificationCodeRepositoryFindOneByContentDto = { content: { key: string; value: string }; - type: VerificationCodeTypeEnum; + code_type: VerificationCodeTypeEnum; }; export interface VerificationCodeRepository { - create(dto: Omit): Promise; + create(dto: VerificationCodeRepositoryCreateDto): Promise; findOne( dto: VerificationCodeRepositoryFilterDto, @@ -25,4 +32,6 @@ export interface VerificationCodeRepository { findOneByContent( dto: VerificationCodeRepositoryFindOneByContentDto, ): Promise; + + removeReservedFields(dto: VerificationCode): T; } diff --git a/src/repositories/session.repository.ts b/src/repositories/session.repository.ts index ec2d086..6c04046 100644 --- a/src/repositories/session.repository.ts +++ b/src/repositories/session.repository.ts @@ -1,5 +1,15 @@ -import { injectable } from 'tsyringe'; +import { + DeleteItemCommand, + type DeleteItemCommandInput, + GetItemCommand, + type GetItemCommandInput, + PutItemCommand, + type PutItemCommandInput, +} from '@aws-sdk/client-dynamodb'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; +import { inject, injectable } from 'tsyringe'; +import type { DynamoConfig } from '@/configs'; import type { Session, SessionRepository as SessionRepositoryInterface, @@ -14,38 +24,68 @@ import type { @injectable() export class SessionRepository implements SessionRepositoryInterface { - private sessions: Session[] = []; + private readonly PK = 'session'; - async upsert(session: Session): Promise { - const index = this.sessions.findIndex( - (userSession) => userSession.user_id === session.user_id, - ); + constructor( + @inject('DynamoConfig') private readonly dynamoConfig: DynamoConfig, + ) {} - if (index !== -1) { - // Update an existing session - this.sessions[index] = session; - } else { - this.sessions.push(session); - } + public async upsert(session: Session): Promise { + const params: PutItemCommandInput = { + TableName: this.dynamoConfig.tableName, + Item: marshall({ + PK: this.PK, + SK: `user_id:${session.user_id}`, + Content: { + access_token: session.access_token, + refresh_token: session.refresh_token, + expires_at: session.expires_at, + user_id: session.user_id, + }, + TTL: this.dynamoConfig.getTTL(new Date(session.expires_at)), + }), + }; + + await this.dynamoConfig.client.send(new PutItemCommand(params)); return session; } - async deleteByUserId({ user_id }: { user_id: string }): Promise { - this.sessions = this.sessions.filter( - (session) => session.user_id !== user_id, - ); + public async deleteByUserId({ user_id }: { user_id: string }): Promise { + const params: DeleteItemCommandInput = { + TableName: this.dynamoConfig.tableName, + Key: marshall({ + PK: this.PK, + SK: `user_id:${user_id}`, + }), + }; + + await this.dynamoConfig.client.send(new DeleteItemCommand(params)); } - async findByUserId({ + public async findByUserId({ user_id, }: { user_id: string; }): Promise { - const session = this.sessions.find( - (userSession) => userSession.user_id === user_id, + const params: GetItemCommandInput = { + TableName: this.dynamoConfig.tableName, + Key: marshall({ + PK: this.PK, + SK: `user_id:${user_id}`, + }), + }; + + const { Item } = await this.dynamoConfig.client.send( + new GetItemCommand(params), ); - return session || null; + if (!Item) { + return null; + } + + const session = unmarshall(Item).Content as Session; + + return session; } } diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index 1e7e572..798dda8 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -1,5 +1,19 @@ +import { + PutItemCommand, + type PutItemCommandInput, + QueryCommand, + type QueryCommandInput, + UpdateItemCommand, + type UpdateItemCommandInput, +} from '@aws-sdk/client-dynamodb'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; import { inject, injectable } from 'tsyringe'; +import { + type DynamoConfig, + DynamoGSIEnum, + DynamoPartitionKeysEnum, +} from '@/configs'; import type { LoggerAdapter, UniqueIdAdapter, @@ -10,37 +24,86 @@ import type { /** DynamoDB structure - PK: user - SK: id:{id} + - ReferenceId: [reference_type]:{reference_value} - Content: { name, email, password, id, created_at } - TTL: INT */ @injectable() export class UserRepository implements UserRepositoryInterface { - private readonly users: User[] = []; + private readonly PK = DynamoPartitionKeysEnum.User; constructor( @inject('UniqueIdAdapter') private readonly uniqueIdAdapter: UniqueIdAdapter, - @inject('LoggerAdapter') - private readonly logger: LoggerAdapter, - ) {} + @inject('DynamoConfig') private readonly dynamoConfig: DynamoConfig, + + @inject('LoggerAdapter') private readonly logger: LoggerAdapter, + ) { + this.logger.setPrefix(this.logger, UserRepository.name); + } public async create(dto: Omit): Promise { - const user = { - id: this.uniqueIdAdapter.generate(), - ...dto, - }; + try { + const user: User = { + id: this.uniqueIdAdapter.generate(), + ...dto, + created_at: new Date().toISOString(), + }; + + const params: PutItemCommandInput = { + TableName: this.dynamoConfig.tableName, + Item: marshall({ + PK: this.PK, + SK: `id:${user.id}`, + ReferenceId: `email:${user.email}`, + Content: user, + }), + }; - this.users.push(user); + await this.dynamoConfig.client.send(new PutItemCommand(params)); - this.logger.debug('User created:', user); + this.logger.debug('User created:', user); - return user; + return user; + } catch (error) { + this.logger.error('Error creating user:', error); + + throw error; + } } public async findByEmail(dto: { email: string }): Promise { - return this.users.find((user) => user.email === dto.email) || null; + try { + const params: QueryCommandInput = { + TableName: this.dynamoConfig.tableName, + IndexName: DynamoGSIEnum.ReferenceIdIndex, + KeyConditionExpression: 'PK = :pk AND ReferenceId = :reference', + ExpressionAttributeValues: marshall({ + ':pk': this.PK, + ':reference': `email:${dto.email}`, + }), + }; + + const { Items } = await this.dynamoConfig.client.send( + new QueryCommand(params), + ); + + if (!Items || Items.length === 0) { + return null; + } + + const user = unmarshall(Items[0]) as { Content: User }; + + this.logger.debug('User found by email:', user); + + return user.Content; + } catch (error) { + this.logger.error('Error finding user by email:', error); + + throw error; + } } public async updateByEmail({ @@ -50,19 +113,45 @@ export class UserRepository implements UserRepositoryInterface { update: Partial>; email: string; }): Promise { - const user = this.users.find((model) => model.email === email); + try { + const user = await this.findByEmail({ email }); - if (!user) { - return; - } + if (!user) { + return this.logger.warn('User not found for update:', email); + } + + const updateExpressions: string[] = []; + const expressionAttributeValues: Record = {}; + const expressionAttributeNames: Record = {}; - const updatedUser = { - ...user, - ...update, - }; + for (const [key, value] of Object.entries(update)) { + const attributeName = `#${key}`; + const attributeValue = `:${key}`; - this.users.splice(this.users.indexOf(user), 1, updatedUser); + updateExpressions.push(`Content.${attributeName} = ${attributeValue}`); + expressionAttributeValues[attributeValue] = value; + expressionAttributeNames[attributeName] = key; + } - this.logger.debug('User updated:', updatedUser); + const params: UpdateItemCommandInput = { + TableName: this.dynamoConfig.tableName, + Key: marshall({ + PK: this.PK, + SK: `id:${user.id}`, + }), + UpdateExpression: `SET ${updateExpressions.join(', ')}`, + ExpressionAttributeValues: marshall(expressionAttributeValues), + ExpressionAttributeNames: expressionAttributeNames, + ReturnValues: 'UPDATED_NEW', + }; + + await this.dynamoConfig.client.send(new UpdateItemCommand(params)); + + this.logger.debug('User updated'); + } catch (error) { + this.logger.error('Error updating user:', error); + + throw error; + } } } diff --git a/src/repositories/verification-code.repository.ts b/src/repositories/verification-code.repository.ts index dad0fb8..c37a650 100644 --- a/src/repositories/verification-code.repository.ts +++ b/src/repositories/verification-code.repository.ts @@ -12,18 +12,20 @@ import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; import { inject, injectable } from 'tsyringe'; import { type DynamoConfig, DynamoPartitionKeysEnum } from '@/configs'; -import type { - LoggerAdapter, - VerificationCode, - VerificationCodeRepositoryFilterDto, - VerificationCodeRepositoryFindOneByContentDto, - VerificationCodeRepository as VerificationCodeRepositoryInterface, +import { + type LoggerAdapter, + type VerificationCode, + type VerificationCodeRepositoryCreateDto, + type VerificationCodeRepositoryFilterDto, + type VerificationCodeRepositoryFindOneByContentDto, + type VerificationCodeRepository as VerificationCodeRepositoryInterface, + VerificationCodeReservedFieldEnum, } from '@/interfaces'; /** DynamoDB structure - PK: verification-code - SK: type:{type}::code:{code} - - Content: { code, type, expires_at, data } + - Content: { code, type, expires_at, ...data } - TTL: INT */ @@ -35,30 +37,31 @@ export class VerificationCodeRepository constructor( @inject('DynamoConfig') private readonly dynamoConfig: DynamoConfig, + @inject('LoggerAdapter') private readonly logger: LoggerAdapter, ) { this.logger.setPrefix(this.logger, VerificationCodeRepository.name); } public async create( - dto: Omit, + dto: VerificationCodeRepositoryCreateDto, ): Promise { try { const code = this.generateCode(6); - const verificationCode = { ...dto, code }; + const verificationCode = { + ...dto.content, + code_expires_at: dto.code_expires_at, + code_type: dto.code_type, + code, + }; const params: PutItemInput = { TableName: this.dynamoConfig.tableName, Item: marshall({ PK: this.PK, - SK: `type:${dto.type}::code:${code}`, - TTL: this.dynamoConfig.getTTL(new Date(dto.expires_at)), - Content: { - ...verificationCode.data, - expires_at: verificationCode.expires_at, - code: verificationCode.code, - type: verificationCode.type, - }, + SK: `type:${dto.code_type}::code:${code}`, + TTL: this.dynamoConfig.getTTL(new Date(dto.code_expires_at)), + Content: verificationCode, }), }; @@ -75,15 +78,15 @@ export class VerificationCodeRepository } public async findOne({ + code_type, code, - type, }: VerificationCodeRepositoryFilterDto): Promise { try { const params: GetItemInput = { TableName: this.dynamoConfig.tableName, Key: marshall({ PK: this.PK, - SK: `type:${type}::code:${code}`, + SK: `type:${code_type}::code:${code}`, }), }; @@ -95,11 +98,13 @@ export class VerificationCodeRepository return null; } - const verificationCode = unmarshall(Item) as VerificationCode; + const verificationCode = unmarshall(Item) as { + Content: VerificationCode; + }; this.logger.debug('Verification code retrieved:', verificationCode); - return verificationCode; + return verificationCode.Content; } catch (error) { this.logger.error('Error retrieving verification code:', error); @@ -109,7 +114,7 @@ export class VerificationCodeRepository public async findOneByContent({ content, - type, + code_type, }: VerificationCodeRepositoryFindOneByContentDto): Promise { try { const params: QueryCommandInput = { @@ -119,7 +124,7 @@ export class VerificationCodeRepository ExpressionAttributeValues: marshall({ ':pk': this.PK, ':value': content.value, - ':type': `type:${type}`, + ':type': `type:${code_type}`, }), ExpressionAttributeNames: { '#key': content.key, @@ -155,22 +160,22 @@ export class VerificationCodeRepository } public async deleteOne({ + code_type, code, - type, }: VerificationCodeRepositoryFilterDto): Promise { try { const params: DeleteItemCommandInput = { TableName: this.dynamoConfig.tableName, Key: marshall({ PK: this.PK, - SK: `type:${type}::code:${code}`, + SK: `type:${code_type}::code:${code}`, }), }; await this.dynamoConfig.client.send(new DeleteItemCommand(params)); this.logger.debug( - `Verification code with code ${code} and type ${type} deleted`, + `Verification code with code ${code} and type ${code_type} deleted`, ); } catch (error) { this.logger.error('Error deleting verification code:', error); @@ -179,6 +184,19 @@ export class VerificationCodeRepository } } + public removeReservedFields(verificationCode: VerificationCode) { + const data = Object.fromEntries( + Object.entries(verificationCode).filter( + ([key]) => + !( + Object.values(VerificationCodeReservedFieldEnum) as string[] + ).includes(key), + ), + ); + + return data as T; + } + private generateCode(length: number): string { const min = Math.pow(10, length - 1); const max = Math.pow(10, length) - 1; diff --git a/src/routers/auth.router.ts b/src/routers/auth.router.ts index 603993f..34c6681 100644 --- a/src/routers/auth.router.ts +++ b/src/routers/auth.router.ts @@ -31,10 +31,20 @@ export class AuthRouter implements Router { ), ], }, - this.authController.login.bind(this.authController), + this.authController.refreshLogin.bind(this.authController), ); - app.delete('/logout', this.authController.logout.bind(this.authController)); + app.post( + '/logout', + { + preHandler: [ + this.ensureAuthenticatedMiddleware.middleware.bind( + this.ensureAuthenticatedMiddleware, + ), + ], + }, + this.authController.logout.bind(this.authController), + ); if (done) { done(); diff --git a/src/services/recovery-password-verify.service.ts b/src/services/recovery-password-verify.service.ts index c480b21..21df8fd 100644 --- a/src/services/recovery-password-verify.service.ts +++ b/src/services/recovery-password-verify.service.ts @@ -22,7 +22,7 @@ export class RecoveryPasswordVerifyService code, }: RecoveryPasswordVerifyServiceDto): Promise { const verificationCode = await this.verificationCodeRepository.findOne({ - type: VerificationCodeTypeEnum.RecoveryPassword, + code_type: VerificationCodeTypeEnum.RecoveryPassword, code, }); diff --git a/src/services/recovery-password.service.ts b/src/services/recovery-password.service.ts index d802845..7f557c7 100644 --- a/src/services/recovery-password.service.ts +++ b/src/services/recovery-password.service.ts @@ -33,14 +33,14 @@ export class RecoveryPasswordService const verificationCodeExists = await this.verificationCodeRepository.findOneByContent({ - type: VerificationCodeTypeEnum.RecoveryPassword, + code_type: VerificationCodeTypeEnum.RecoveryPassword, content: { key: 'email', value: email }, }); if (verificationCodeExists) { await this.verificationCodeRepository.deleteOne({ code: verificationCodeExists.code, - type: VerificationCodeTypeEnum.RecoveryPassword, + code_type: VerificationCodeTypeEnum.RecoveryPassword, }); } @@ -50,8 +50,8 @@ export class RecoveryPasswordService ).toISOString(); const verificationCode = await this.verificationCodeRepository.create({ - type: VerificationCodeTypeEnum.RecoveryPassword, - expires_at: expiresAt, + code_type: VerificationCodeTypeEnum.RecoveryPassword, + code_expires_at: expiresAt, content: { email }, }); diff --git a/src/services/refresh-login.service.ts b/src/services/refresh-login.service.ts index e3443f0..49b2a4d 100644 --- a/src/services/refresh-login.service.ts +++ b/src/services/refresh-login.service.ts @@ -5,6 +5,7 @@ import { HttpStatusCodesEnum } from '@/constants'; import { AppError } from '@/errors'; import type { AuthHelper, + LoggerAdapter, RefreshLoginServiceDto, RefreshLoginService as RefreshLoginServiceInterface, Session, @@ -22,22 +23,18 @@ export class RefreshLoginService implements RefreshLoginServiceInterface { @inject('AuthHelper') private readonly authHelper: AuthHelper, + + @inject('LoggerAdapter') + private readonly logger: LoggerAdapter, ) {} public async execute({ refresh_token: refreshToken, }: RefreshLoginServiceDto): Promise> { - const decodedToken = this.jwtConfig.verify<{ sub?: string }>(refreshToken); - - if (!decodedToken?.sub) { - throw new AppError({ - status_code: HttpStatusCodesEnum.UNAUTHORIZED, - message: 'Unauthorized', - }); - } + const userId = this.getUserId({ refreshToken }); const session = await this.sessionRepository.findByUserId({ - user_id: decodedToken.sub, + user_id: userId, }); if ( @@ -57,4 +54,28 @@ export class RefreshLoginService implements RefreshLoginServiceInterface { return refreshedSession; } + + private getUserId({ refreshToken }: { refreshToken: string }): string { + try { + const decodedToken = this.jwtConfig.verify<{ sub?: string }>( + refreshToken, + ); + + if (!decodedToken?.sub) { + throw new AppError({ + status_code: HttpStatusCodesEnum.UNAUTHORIZED, + message: 'Unauthorized', + }); + } + + return decodedToken.sub; + } catch (error) { + this.logger.error('Error while verifying token', error); + + throw new AppError({ + status_code: HttpStatusCodesEnum.UNAUTHORIZED, + message: 'Unauthorized', + }); + } + } } diff --git a/src/services/registration-confirm.service.ts b/src/services/registration-confirm.service.ts index 4f6a22b..30e9023 100644 --- a/src/services/registration-confirm.service.ts +++ b/src/services/registration-confirm.service.ts @@ -7,6 +7,7 @@ import { type RegistrationConfirmService as RegistrationConfirmServiceInterface, type User, type UserRepository, + type VerificationCode, type VerificationCodeRepository, VerificationCodeTypeEnum, } from '@/interfaces'; @@ -25,7 +26,7 @@ export class RegistrationConfirmService public async execute({ code }: RegistrationConfirmServiceDto): Promise { const verificationCode = await this.verificationCodeRepository.findOne({ - type: VerificationCodeTypeEnum.Registration, + code_type: VerificationCodeTypeEnum.Registration, code, }); @@ -36,7 +37,7 @@ export class RegistrationConfirmService }); } - const user = this.parseUser(verificationCode.content); + const user = this.parseUser(verificationCode); const userExists = await this.userRepository.findByEmail({ email: user.email, @@ -52,10 +53,14 @@ export class RegistrationConfirmService await this.userRepository.create(user); } - private parseUser( - content: Record, - ): Omit { - const user = content as Omit; + private parseUser(verificationCode: VerificationCode): Omit { + const userData = + this.verificationCodeRepository.removeReservedFields>( + verificationCode, + ); + + // To do: Validate the user data + const user = userData as Omit; return user; } diff --git a/src/services/registration.service.ts b/src/services/registration.service.ts index 62b16a6..b97f11c 100644 --- a/src/services/registration.service.ts +++ b/src/services/registration.service.ts @@ -43,13 +43,13 @@ export class RegistrationService implements RegistrationServiceInterface { const verificationCodeExists = await this.verificationCodeRepository.findOneByContent({ - type: VerificationCodeTypeEnum.Registration, + code_type: VerificationCodeTypeEnum.Registration, content: { key: 'email', value: email }, }); if (verificationCodeExists) { await this.verificationCodeRepository.deleteOne({ - type: VerificationCodeTypeEnum.Registration, + code_type: VerificationCodeTypeEnum.Registration, code: verificationCodeExists.code, }); } @@ -60,7 +60,7 @@ export class RegistrationService implements RegistrationServiceInterface { salt, }); - const data: Omit = { + const content: Omit = { password: hashedPassword, password_salt: salt, email, @@ -73,9 +73,9 @@ export class RegistrationService implements RegistrationServiceInterface { ).toISOString(); await this.verificationCodeRepository.create({ - type: VerificationCodeTypeEnum.Registration, - expires_at: expiresAt, - data, + code_type: VerificationCodeTypeEnum.Registration, + code_expires_at: expiresAt, + content, }); } } diff --git a/src/services/reset-password.service.ts b/src/services/reset-password.service.ts index c2c12f9..2ea06ba 100644 --- a/src/services/reset-password.service.ts +++ b/src/services/reset-password.service.ts @@ -7,6 +7,7 @@ import { type ResetPasswordServiceDto, type ResetPasswordService as ResetPasswordServiceInterface, type UserRepository, + type VerificationCode, type VerificationCodeRepository, VerificationCodeTypeEnum, } from '@/interfaces'; @@ -30,11 +31,14 @@ export class ResetPasswordService implements ResetPasswordServiceInterface { code, }: ResetPasswordServiceDto): Promise { const verificationCode = await this.verificationCodeRepository.findOne({ - type: VerificationCodeTypeEnum.RecoveryPassword, + code_type: VerificationCodeTypeEnum.RecoveryPassword, code, }); - if (!verificationCode || verificationCode.content.email !== email) { + if ( + !verificationCode || + this.parseVerificationCode(verificationCode).email !== email + ) { throw new AppError({ status_code: HttpStatusCodesEnum.UNAUTHORIZED, message: 'Unauthorized', @@ -54,5 +58,21 @@ export class ResetPasswordService implements ResetPasswordServiceInterface { password_salt: salt, }, }); + + await this.verificationCodeRepository.deleteOne({ + code_type: verificationCode.code_type, + code: verificationCode.code, + }); + } + + private parseVerificationCode(verificationCode: VerificationCode) { + const data = this.verificationCodeRepository.removeReservedFields<{ + email: string; + }>(verificationCode); + + // To do: Validate the email + const email = data.email || null; + + return { email }; } }