From e6bfb24344669ee5d5bcf4cc429a9f6f62136ba2 Mon Sep 17 00:00:00 2001 From: ribeirogab Date: Sat, 21 Sep 2024 02:53:37 -0300 Subject: [PATCH 1/2] feat: magic link/code to login and register --- .env.example | 8 +- src/configs/env.config.ts | 5 ++ src/configs/jwt.config.ts | 30 +++++-- src/constants.ts | 5 +- src/container.ts | 2 + src/controllers/auth.controller.ts | 25 ++++-- src/controllers/registration.controller.ts | 13 ++- src/errors/app.error.ts | 12 ++- src/interfaces/index.ts | 1 + src/interfaces/models/user.ts | 2 - src/interfaces/models/verification-code.ts | 3 + .../verification-code.repository.ts | 2 +- .../services/login-confirm.service.ts | 10 +++ src/interfaces/services/login.service.ts | 5 +- .../services/registration-confirm.service.ts | 1 + .../services/registration.service.ts | 3 +- .../ensure-authenticated.middleware.ts | 11 ++- src/middlewares/error-handling.middleware.ts | 6 +- .../verification-code.repository.ts | 23 +++-- src/routers/auth.router.ts | 5 ++ src/services/index.ts | 1 + src/services/login-confirm.service.ts | 84 +++++++++++++++++++ src/services/login.service.ts | 79 +++++++++-------- src/services/registration-confirm.service.ts | 62 ++++++++++++-- src/services/registration.service.ts | 34 ++++---- 25 files changed, 327 insertions(+), 105 deletions(-) create mode 100644 src/interfaces/services/login-confirm.service.ts create mode 100644 src/services/login-confirm.service.ts diff --git a/.env.example b/.env.example index b56fe03..9670abe 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,12 @@ -# Application +# Environment NODE_ENV=development STAGE=dev -JWT_SECRET=secret + +# Application +JWT_SECRET=auth-jwt-secret +JWT_SECRET_VERIFICATION_TOKEN=verification-token-jwt-secret # AWS AWS_REGION=us-east-1 AWS_DYNAMO_TABLE_NAME=dev-authentication + diff --git a/src/configs/env.config.ts b/src/configs/env.config.ts index 7e70034..253dba1 100644 --- a/src/configs/env.config.ts +++ b/src/configs/env.config.ts @@ -25,6 +25,11 @@ export class EnvConfig { public readonly PORT = get('PORT').default(8080).asPortNumber(); public readonly CORS_ORIGIN = get('CORS_ORIGIN').default('*').asString(); public readonly JWT_SECRET = get('JWT_SECRET').required().asString(); + public readonly JWT_SECRET_VERIFICATION_TOKEN = get( + 'JWT_SECRET_VERIFICATION_TOKEN', + ) + .required() + .asString(); public readonly EMAIL_PROVIDER = get('EMAIL_PROVIDER') .default(EmailProviderEnum.Resend) diff --git a/src/configs/jwt.config.ts b/src/configs/jwt.config.ts index 8bec02c..8910eec 100644 --- a/src/configs/jwt.config.ts +++ b/src/configs/jwt.config.ts @@ -2,6 +2,7 @@ import { sign, verify } from 'jsonwebtoken'; import { inject, injectable } from 'tsyringe'; import type { EnvConfig } from './env.config'; +import type { LoggerAdapter } from '@/interfaces'; type SignInDto = { /** @@ -10,20 +11,39 @@ type SignInDto = { */ expiresIn?: string | number; subject?: string; + secret?: string; }; @injectable() export class JwtConfig { - constructor(@inject('EnvConfig') private readonly envConfig: EnvConfig) {} + constructor( + @inject('LoggerAdapter') private readonly logger: LoggerAdapter, - public sign({ expiresIn, subject }: SignInDto) { - return sign({}, this.envConfig.JWT_SECRET, { + @inject('EnvConfig') private readonly envConfig: EnvConfig, + ) { + this.logger = this.logger.setPrefix(this.logger, JwtConfig.name); + } + + public sign({ expiresIn, subject, secret }: SignInDto) { + return sign({}, secret || this.envConfig.JWT_SECRET, { expiresIn, subject, }); } - public verify(token: string) { - return verify(token, this.envConfig.JWT_SECRET) as T; + public verify({ + token, + secret, + }: { + token: string; + secret?: string; + }): T | null { + try { + return verify(token, secret || this.envConfig.JWT_SECRET) as T; + } catch (error) { + this.logger.error('Error while verifying token', error); + + return null; + } } } diff --git a/src/constants.ts b/src/constants.ts index ac76ed1..9b20037 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,9 +9,12 @@ export enum HttpMethodEnum { } export enum AppErrorCodeEnum { - VerificationCodeNotFound = 'verification_code_not_found', + VerificationCodeInvalidOrExpired = 'verification_code_invalid_or_expired', RegisterTokenNotFound = 'register_token_not_found', EmailAlreadyInUse = 'email_already_in_use', + ValidationError = 'validation_error', + InvalidLogin = 'invalid_login', + Unknown = 'unknown', } export enum NodeEnvEnum { diff --git a/src/container.ts b/src/container.ts index cc80f7f..85c0448 100644 --- a/src/container.ts +++ b/src/container.ts @@ -8,6 +8,7 @@ import { EnsureAuthenticatedMiddleware, ErrorHandlingMiddleware, RequestAuditMid import { EmailTemplateRepository, SessionRepository, UserRepository, VerificationCodeRepository } from './repositories'; import { AppRouter, AuthRouter, PasswordRouter, RegistrationRouter } from './routers'; import { + LoginConfirmService, LoginService, LogoutService, RecoveryPasswordService, @@ -51,6 +52,7 @@ container.registerSingleton('RecoveryPasswordService', container.registerSingleton('ResetPasswordService', ResetPasswordService); container.registerSingleton('RegistrationService', RegistrationService); container.registerSingleton('RefreshLoginService', RefreshLoginService); +container.registerSingleton('LoginConfirmService', LoginConfirmService); container.registerSingleton('LogoutService', LogoutService); container.registerSingleton('LoginService', LoginService); diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 519ce0c..1121fd3 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -3,6 +3,8 @@ import { inject, injectable } from 'tsyringe'; import { HttpStatusCodesEnum } from '@/constants'; import type { + LoginConfirmService, + LoginConfirmServiceDto, LoginService, LoginServiceDto, LogoutService, @@ -12,20 +14,31 @@ import type { @injectable() export class AuthController { constructor( - @inject('LoginService') - private readonly loginService: LoginService, + @inject('RefreshLoginService') + private readonly refreshLoginService: RefreshLoginService, + + @inject('LoginConfirmService') + private readonly loginConfirmService: LoginConfirmService, @inject('LogoutService') private readonly logoutService: LogoutService, - @inject('RefreshLoginService') - private readonly refreshLoginService: RefreshLoginService, + @inject('LoginService') + private readonly loginService: LoginService, ) {} public async login(request: FastifyRequest, reply: FastifyReply) { - const { email, password } = request.body as LoginServiceDto; + const { email } = request.body as LoginServiceDto; + + const response = await this.loginService.execute({ email }); + + return reply.code(HttpStatusCodesEnum.OK).send(response); + } + + public async loginConfirm(request: FastifyRequest, reply: FastifyReply) { + const { code, token } = request.body as LoginConfirmServiceDto; - const response = await this.loginService.execute({ email, password }); + const response = await this.loginConfirmService.execute({ code, token }); return reply.code(HttpStatusCodesEnum.OK).send(response); } diff --git a/src/controllers/registration.controller.ts b/src/controllers/registration.controller.ts index d48f3c2..c056aed 100644 --- a/src/controllers/registration.controller.ts +++ b/src/controllers/registration.controller.ts @@ -20,23 +20,20 @@ export class RegistrationController { ) {} public async registration(request: FastifyRequest, reply: FastifyReply) { - const { email, name, password } = request.body as Omit< - User, - 'id' | 'password_salt' - >; + const { email, name } = request.body as Omit; - await this.registrationService.execute({ email, name, password }); + const response = await this.registrationService.execute({ email, name }); - return reply.code(HttpStatusCodesEnum.NO_CONTENT).send(); + return reply.code(HttpStatusCodesEnum.OK).send(response); } public async registrationConfirm( request: FastifyRequest, reply: FastifyReply, ) { - const { code } = request.body as RegistrationConfirmServiceDto; + const { code, token } = request.body as RegistrationConfirmServiceDto; - await this.registrationConfirmService.execute({ code }); + await this.registrationConfirmService.execute({ code, token }); return reply.code(HttpStatusCodesEnum.NO_CONTENT).send(); } diff --git a/src/errors/app.error.ts b/src/errors/app.error.ts index 4cb87cc..9c7e9b0 100644 --- a/src/errors/app.error.ts +++ b/src/errors/app.error.ts @@ -1,18 +1,26 @@ -import type { HttpStatusCodesEnum } from '@/constants'; +import { AppErrorCodeEnum, type HttpStatusCodesEnum } from '@/constants'; export type AppErrorConstructor = { status_code: HttpStatusCodesEnum; + error_code?: AppErrorCodeEnum; details?: unknown; message: string; }; export class AppError extends Error { public readonly status_code: HttpStatusCodesEnum; + public readonly error_code?: AppErrorCodeEnum; public readonly details?: unknown; - constructor({ message, status_code, details }: AppErrorConstructor) { + constructor({ + error_code = AppErrorCodeEnum.Unknown, + status_code, + message, + details, + }: AppErrorConstructor) { super(message); this.status_code = status_code; + this.error_code = error_code; this.details = details; } } diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 1f06f6a..a1ed75d 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -32,6 +32,7 @@ export * from './services/recovery-password-verify.service'; export * from './services/registration-confirm.service'; export * from './services/recovery-password.service'; export * from './services/reset-password.service'; +export * from './services/login-confirm.service'; export * from './services/refresh-login.service'; export * from './services/registration.service'; export * from './services/logout.service'; diff --git a/src/interfaces/models/user.ts b/src/interfaces/models/user.ts index fe504b1..76ffb85 100644 --- a/src/interfaces/models/user.ts +++ b/src/interfaces/models/user.ts @@ -2,8 +2,6 @@ export type User = { id: string; name: string; email: string; - password: string; - password_salt: string; created_at?: string; updated_at?: string; }; diff --git a/src/interfaces/models/verification-code.ts b/src/interfaces/models/verification-code.ts index b5e3873..6594ade 100644 --- a/src/interfaces/models/verification-code.ts +++ b/src/interfaces/models/verification-code.ts @@ -1,16 +1,19 @@ export enum VerificationCodeTypeEnum { RecoveryPassword = 'recovery-password', Registration = 'registration', + Login = 'login', } export enum VerificationCodeReservedFieldEnum { code_expires_at = 'code_expires_at', code_type = 'code_type', + token = 'token', code = 'code', } export type VerificationCode = { code_type: VerificationCodeTypeEnum; code_expires_at: string; + token: string; code: string; }; diff --git a/src/interfaces/repositories/verification-code.repository.ts b/src/interfaces/repositories/verification-code.repository.ts index b476691..5fb24cf 100644 --- a/src/interfaces/repositories/verification-code.repository.ts +++ b/src/interfaces/repositories/verification-code.repository.ts @@ -5,7 +5,7 @@ import type { export type VerificationCodeRepositoryFilterDto = { code_type: VerificationCodeTypeEnum; - code: string; + token: string; }; export type VerificationCodeRepositoryCreateDto = Omit< diff --git a/src/interfaces/services/login-confirm.service.ts b/src/interfaces/services/login-confirm.service.ts new file mode 100644 index 0000000..07099e1 --- /dev/null +++ b/src/interfaces/services/login-confirm.service.ts @@ -0,0 +1,10 @@ +import type { Session } from '../models/session'; + +export type LoginConfirmServiceDto = { + token: string; + code: string; +}; + +export interface LoginConfirmService { + execute(dto: LoginConfirmServiceDto): Promise>; +} diff --git a/src/interfaces/services/login.service.ts b/src/interfaces/services/login.service.ts index 7f1be2f..ced4f6a 100644 --- a/src/interfaces/services/login.service.ts +++ b/src/interfaces/services/login.service.ts @@ -1,10 +1,7 @@ -import type { Session } from '../models/session'; - export type LoginServiceDto = { email: string; - password: string; }; export interface LoginService { - execute(dto: LoginServiceDto): Promise>; + execute(dto: LoginServiceDto): Promise<{ token: string }>; } diff --git a/src/interfaces/services/registration-confirm.service.ts b/src/interfaces/services/registration-confirm.service.ts index 2dffbe3..0c65733 100644 --- a/src/interfaces/services/registration-confirm.service.ts +++ b/src/interfaces/services/registration-confirm.service.ts @@ -1,4 +1,5 @@ export type RegistrationConfirmServiceDto = { + token: string; code: string; }; diff --git a/src/interfaces/services/registration.service.ts b/src/interfaces/services/registration.service.ts index a946359..6b9b93c 100644 --- a/src/interfaces/services/registration.service.ts +++ b/src/interfaces/services/registration.service.ts @@ -1,9 +1,8 @@ export type RegistrationServiceDto = { - password: string; email: string; name: string; }; export interface RegistrationService { - execute(dto: RegistrationServiceDto): Promise; + execute(dto: RegistrationServiceDto): Promise<{ token: string }>; } diff --git a/src/middlewares/ensure-authenticated.middleware.ts b/src/middlewares/ensure-authenticated.middleware.ts index 3d1d16c..185d049 100644 --- a/src/middlewares/ensure-authenticated.middleware.ts +++ b/src/middlewares/ensure-authenticated.middleware.ts @@ -34,9 +34,16 @@ export class EnsureAuthenticatedMiddleware implements HookMiddleware { const [, token] = authorization.split(' '); try { - const { sub } = this.jwtConfig.verify<{ sub: string }>(token); + const decoded = this.jwtConfig.verify<{ sub: string }>({ token }); - request.user = { id: sub }; + if (!decoded) { + throw new AppError({ + status_code: HttpStatusCodesEnum.UNAUTHORIZED, + message: 'Unauthorized', + }); + } + + request.user = { id: decoded.sub }; } catch (error) { this.logger.error('Error while verifying token', error); diff --git a/src/middlewares/error-handling.middleware.ts b/src/middlewares/error-handling.middleware.ts index e7743ea..a65b4cc 100644 --- a/src/middlewares/error-handling.middleware.ts +++ b/src/middlewares/error-handling.middleware.ts @@ -2,7 +2,7 @@ import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify'; import { inject, injectable } from 'tsyringe'; import { ZodError } from 'zod'; -import { HttpStatusCodesEnum } from '@/constants'; +import { AppErrorCodeEnum, HttpStatusCodesEnum } from '@/constants'; import { AppError } from '@/errors'; import type { ErrorMiddleware, LoggerAdapter } from '@/interfaces'; @@ -20,6 +20,7 @@ export class ErrorHandlingMiddleware implements ErrorMiddleware { if (error instanceof AppError) { return reply.status(error.status_code).send({ status_code: error.status_code, + error_code: error.error_code, message: error.message, details: error.details, }); @@ -28,6 +29,7 @@ export class ErrorHandlingMiddleware implements ErrorMiddleware { if (error instanceof ZodError) { const errorBody = { status_code: HttpStatusCodesEnum.BAD_REQUEST, + error_code: AppErrorCodeEnum.ValidationError, message: 'Payload validation error', details: error.issues, }; @@ -46,6 +48,7 @@ export class ErrorHandlingMiddleware implements ErrorMiddleware { error.statusCode !== HttpStatusCodesEnum.INTERNAL_SERVER_ERROR ) { const errorBody = { + error_code: AppErrorCodeEnum.Unknown, status_code: error.statusCode, message: error.message, }; @@ -59,6 +62,7 @@ export class ErrorHandlingMiddleware implements ErrorMiddleware { return reply.status(HttpStatusCodesEnum.INTERNAL_SERVER_ERROR).send({ status_code: HttpStatusCodesEnum.INTERNAL_SERVER_ERROR, + error_code: AppErrorCodeEnum.Unknown, message: 'Internal server error', }); } diff --git a/src/repositories/verification-code.repository.ts b/src/repositories/verification-code.repository.ts index c37a650..cc0d2d5 100644 --- a/src/repositories/verification-code.repository.ts +++ b/src/repositories/verification-code.repository.ts @@ -24,8 +24,8 @@ import { /** DynamoDB structure - PK: verification-code - - SK: type:{type}::code:{code} - - Content: { code, type, expires_at, ...data } + - SK: type:{type}::token:{token} + - Content: { code, type, expires_at, token, ...data } - TTL: INT */ @@ -52,6 +52,7 @@ export class VerificationCodeRepository ...dto.content, code_expires_at: dto.code_expires_at, code_type: dto.code_type, + token: dto.token, code, }; @@ -59,7 +60,7 @@ export class VerificationCodeRepository TableName: this.dynamoConfig.tableName, Item: marshall({ PK: this.PK, - SK: `type:${dto.code_type}::code:${code}`, + SK: this.getSortKey(dto.code_type, dto.token), TTL: this.dynamoConfig.getTTL(new Date(dto.code_expires_at)), Content: verificationCode, }), @@ -79,14 +80,14 @@ export class VerificationCodeRepository public async findOne({ code_type, - code, + token, }: VerificationCodeRepositoryFilterDto): Promise { try { const params: GetItemInput = { TableName: this.dynamoConfig.tableName, Key: marshall({ PK: this.PK, - SK: `type:${code_type}::code:${code}`, + SK: this.getSortKey(code_type, token), }), }; @@ -113,8 +114,8 @@ export class VerificationCodeRepository } public async findOneByContent({ - content, code_type, + content, }: VerificationCodeRepositoryFindOneByContentDto): Promise { try { const params: QueryCommandInput = { @@ -161,21 +162,21 @@ export class VerificationCodeRepository public async deleteOne({ code_type, - code, + token, }: VerificationCodeRepositoryFilterDto): Promise { try { const params: DeleteItemCommandInput = { TableName: this.dynamoConfig.tableName, Key: marshall({ PK: this.PK, - SK: `type:${code_type}::code:${code}`, + SK: this.getSortKey(code_type, token), }), }; await this.dynamoConfig.client.send(new DeleteItemCommand(params)); this.logger.debug( - `Verification code with code ${code} and type ${code_type} deleted`, + `Verification code with token ${token} and type ${code_type} deleted`, ); } catch (error) { this.logger.error('Error deleting verification code:', error); @@ -203,4 +204,8 @@ export class VerificationCodeRepository return Math.floor(min + Math.random() * (max - min + 1)).toString(); } + + private getSortKey(codeType: string, token: string): string { + return `type:${codeType}::token:${token}`; + } } diff --git a/src/routers/auth.router.ts b/src/routers/auth.router.ts index 34c6681..b16c808 100644 --- a/src/routers/auth.router.ts +++ b/src/routers/auth.router.ts @@ -22,6 +22,11 @@ export class AuthRouter implements Router { ) { app.post('/login', this.authController.login.bind(this.authController)); + app.post( + '/login/confirm', + this.authController.loginConfirm.bind(this.authController), + ); + app.post( '/login/refresh', { diff --git a/src/services/index.ts b/src/services/index.ts index a1d5473..9a8f794 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -3,6 +3,7 @@ export * from './registration-confirm.service'; export * from './recovery-password.service'; export * from './reset-password.service'; export * from './refresh-login.service'; +export * from './login-confirm.service'; export * from './registration.service'; export * from './logout.service'; export * from './login.service'; diff --git a/src/services/login-confirm.service.ts b/src/services/login-confirm.service.ts new file mode 100644 index 0000000..6282ad7 --- /dev/null +++ b/src/services/login-confirm.service.ts @@ -0,0 +1,84 @@ +import { inject, injectable } from 'tsyringe'; + +import type { EnvConfig, JwtConfig } from '@/configs'; +import { AppErrorCodeEnum, HttpStatusCodesEnum } from '@/constants'; +import { AppError } from '@/errors'; +import { + type AuthHelper, + type LoginConfirmServiceDto, + type LoginConfirmService as LoginConfirmServiceInterface, + type Session, + type UserRepository, + type VerificationCodeRepository, + VerificationCodeTypeEnum, +} from '@/interfaces'; + +@injectable() +export class LoginConfirmService implements LoginConfirmServiceInterface { + constructor( + @inject('VerificationCodeRepository') + private verificationCodeRepository: VerificationCodeRepository, + + @inject('UserRepository') + private readonly userRepository: UserRepository, + + @inject('AuthHelper') + private readonly authHelper: AuthHelper, + + @inject('JwtConfig') + private readonly jwtConfig: JwtConfig, + + @inject('EnvConfig') + private readonly envConfig: EnvConfig, + ) {} + + private get genericAuthError() { + return new AppError({ + status_code: HttpStatusCodesEnum.UNAUTHORIZED, + error_code: AppErrorCodeEnum.InvalidLogin, + message: 'Unauthorized', + }); + } + + public async execute({ + token, + code, + }: LoginConfirmServiceDto): Promise> { + const decoded = this.jwtConfig.verify<{ sub: string }>({ + secret: this.envConfig.JWT_SECRET_VERIFICATION_TOKEN, + token, + }); + + if (!decoded) { + throw this.genericAuthError; + } + + const verificationCode = await this.verificationCodeRepository.findOne({ + code_type: VerificationCodeTypeEnum.Login, + token, + }); + + if (!verificationCode || verificationCode.code !== code) { + throw this.genericAuthError; + } + + const user = await this.userRepository.findByEmail({ email: decoded.sub }); + + if (!user) { + throw this.genericAuthError; + } + + const session = await this.authHelper.createSession({ user_id: user.id }); + + await this.verificationCodeRepository.deleteOne({ + code_type: VerificationCodeTypeEnum.Login, + token, + }); + + return { + refresh_token: session.refresh_token, + access_token: session.access_token, + expires_at: session.expires_at, + }; + } +} diff --git a/src/services/login.service.ts b/src/services/login.service.ts index 116203a..2647768 100644 --- a/src/services/login.service.ts +++ b/src/services/login.service.ts @@ -1,61 +1,68 @@ import { inject, injectable } from 'tsyringe'; -import { HttpStatusCodesEnum } from '@/constants'; -import { AppError } from '@/errors'; -import type { - AuthHelper, - HashAdapter, - LoginServiceDto, - LoginService as LoginServiceInterface, - Session, - UserRepository, +import type { EnvConfig, JwtConfig } from '@/configs'; +import { + type LoginServiceDto, + type LoginService as LoginServiceInterface, + type UserRepository, + type VerificationCodeRepository, + VerificationCodeTypeEnum, } from '@/interfaces'; @injectable() export class LoginService implements LoginServiceInterface { constructor( + @inject('VerificationCodeRepository') + private verificationCodeRepository: VerificationCodeRepository, + @inject('UserRepository') private readonly userRepository: UserRepository, - @inject('HashAdapter') - private readonly hashAdapter: HashAdapter, + @inject('JwtConfig') + private readonly jwtConfig: JwtConfig, - @inject('AuthHelper') - private readonly authHelper: AuthHelper, + @inject('EnvConfig') + private readonly envConfig: EnvConfig, ) {} - public async execute({ - password, - email, - }: LoginServiceDto): Promise> { + public async execute({ email }: LoginServiceDto): Promise<{ token: string }> { const user = await this.userRepository.findByEmail({ email }); + const token = this.jwtConfig.sign({ + secret: this.envConfig.JWT_SECRET_VERIFICATION_TOKEN, + expiresIn: '10m', + subject: email, + }); + if (!user) { - throw new AppError({ - status_code: HttpStatusCodesEnum.UNAUTHORIZED, - message: 'Unauthorized', - }); + return { token }; } - const passwordsMatch = this.hashAdapter.compare({ - decrypted: password, - encrypted: user.password, - salt: user.password_salt, - }); + const verificationCodeExists = + await this.verificationCodeRepository.findOneByContent({ + code_type: VerificationCodeTypeEnum.Registration, + content: { key: 'email', value: email }, + }); - if (!passwordsMatch) { - throw new AppError({ - status_code: HttpStatusCodesEnum.UNAUTHORIZED, - message: 'Unauthorized', + if (verificationCodeExists) { + await this.verificationCodeRepository.deleteOne({ + code_type: VerificationCodeTypeEnum.Registration, + token: verificationCodeExists.token, }); } - const session = await this.authHelper.createSession({ user_id: user.id }); + const expirationTimeInMinutes = 10; // 10 minutes + const expiresAt = new Date( + Date.now() + expirationTimeInMinutes * 60 * 1000, + ).toISOString(); + + await this.verificationCodeRepository.create({ + code_type: VerificationCodeTypeEnum.Login, + code_expires_at: expiresAt, + content: { email }, + token, + }); - return { - refresh_token: session.refresh_token, - access_token: session.access_token, - expires_at: session.expires_at, - }; + return { token }; } } diff --git a/src/services/registration-confirm.service.ts b/src/services/registration-confirm.service.ts index 30e9023..b6b0edf 100644 --- a/src/services/registration-confirm.service.ts +++ b/src/services/registration-confirm.service.ts @@ -1,5 +1,6 @@ import { inject, injectable } from 'tsyringe'; +import type { EnvConfig, JwtConfig } from '@/configs'; import { AppErrorCodeEnum, HttpStatusCodesEnum } from '@/constants'; import { AppError } from '@/errors'; import { @@ -22,18 +23,28 @@ export class RegistrationConfirmService @inject('VerificationCodeRepository') private verificationCodeRepository: VerificationCodeRepository, + + @inject('JwtConfig') + private readonly jwtConfig: JwtConfig, + + @inject('EnvConfig') + private readonly envConfig: EnvConfig, ) {} - public async execute({ code }: RegistrationConfirmServiceDto): Promise { - const verificationCode = await this.verificationCodeRepository.findOne({ - code_type: VerificationCodeTypeEnum.Registration, - code, - }); + public async execute({ + token, + code, + }: RegistrationConfirmServiceDto): Promise { + this.jwtConfig.verify<{ sub: string }>({ token }); + + const verificationCode = await this.getVerificationToken({ token }); - if (!verificationCode) { + if (!verificationCode || verificationCode.code !== code) { throw new AppError({ - message: AppErrorCodeEnum.VerificationCodeNotFound, - status_code: HttpStatusCodesEnum.NOT_FOUND, + error_code: AppErrorCodeEnum.VerificationCodeInvalidOrExpired, + status_code: HttpStatusCodesEnum.UNAUTHORIZED, + message: + 'This code has been used or expired. Please go back to get a new code.', }); } @@ -45,12 +56,45 @@ export class RegistrationConfirmService if (userExists) { throw new AppError({ - message: AppErrorCodeEnum.EmailAlreadyInUse, + error_code: AppErrorCodeEnum.EmailAlreadyInUse, status_code: HttpStatusCodesEnum.CONFLICT, + message: 'This email is already in use.', }); } await this.userRepository.create(user); + + await this.verificationCodeRepository.deleteOne({ + code_type: VerificationCodeTypeEnum.Registration, + token, + }); + } + + private async getVerificationToken({ + token, + }: { + token: string; + }): Promise { + const decoded = this.jwtConfig.verify<{ sub: string }>({ + secret: this.envConfig.JWT_SECRET_VERIFICATION_TOKEN, + token, + }); + + if (!decoded) { + throw new AppError({ + error_code: AppErrorCodeEnum.VerificationCodeInvalidOrExpired, + status_code: HttpStatusCodesEnum.UNAUTHORIZED, + message: + 'This code has been used or expired. Please go back to get a new code.', + }); + } + + const verificationCode = await this.verificationCodeRepository.findOne({ + code_type: VerificationCodeTypeEnum.Registration, + token, + }); + + return verificationCode; } private parseUser(verificationCode: VerificationCode): Omit { diff --git a/src/services/registration.service.ts b/src/services/registration.service.ts index b97f11c..10f8212 100644 --- a/src/services/registration.service.ts +++ b/src/services/registration.service.ts @@ -1,6 +1,6 @@ import { inject, injectable } from 'tsyringe'; -import type { HashAdapter } from '@/adapters'; +import type { EnvConfig, JwtConfig } from '@/configs'; import { AppErrorCodeEnum, HttpStatusCodesEnum } from '@/constants'; import { AppError } from '@/errors'; import { @@ -21,23 +21,26 @@ export class RegistrationService implements RegistrationServiceInterface { @inject('VerificationCodeRepository') private verificationCodeRepository: VerificationCodeRepository, - @inject('HashAdapter') - private hashAdapter: HashAdapter, + @inject('JwtConfig') + private readonly jwtConfig: JwtConfig, + + @inject('EnvConfig') + private readonly envConfig: EnvConfig, ) {} public async execute({ - password, email, name, - }: RegistrationServiceDto): Promise { + }: RegistrationServiceDto): Promise<{ token: string }> { const userExists = await this.userRepository.findByEmail({ email, }); if (userExists) { throw new AppError({ - message: AppErrorCodeEnum.EmailAlreadyInUse, + error_code: AppErrorCodeEnum.EmailAlreadyInUse, status_code: HttpStatusCodesEnum.CONFLICT, + message: 'This email is already in use.', }); } @@ -50,19 +53,11 @@ export class RegistrationService implements RegistrationServiceInterface { if (verificationCodeExists) { await this.verificationCodeRepository.deleteOne({ code_type: VerificationCodeTypeEnum.Registration, - code: verificationCodeExists.code, + token: verificationCodeExists.token, }); } - const salt = this.hashAdapter.generateSalt(); - const hashedPassword = this.hashAdapter.hash({ - text: password, - salt, - }); - const content: Omit = { - password: hashedPassword, - password_salt: salt, email, name, }; @@ -72,10 +67,19 @@ export class RegistrationService implements RegistrationServiceInterface { Date.now() + expirationTimeInMinutes * 60 * 1000, ).toISOString(); + const token = this.jwtConfig.sign({ + secret: this.envConfig.JWT_SECRET_VERIFICATION_TOKEN, + expiresIn: '1h', + subject: email, + }); + await this.verificationCodeRepository.create({ code_type: VerificationCodeTypeEnum.Registration, code_expires_at: expiresAt, content, + token, }); + + return { token }; } } From 045d5dc5e4ab27ec539be3724b0b2c00ba68c3cc Mon Sep 17 00:00:00 2001 From: ribeirogab Date: Sat, 21 Sep 2024 03:08:04 -0300 Subject: [PATCH 2/2] feat: improve routes and remove unused code --- src/adapters/hash/crypto.provider.ts | 35 ---------- src/adapters/hash/index.ts | 38 ----------- src/adapters/index.ts | 1 - src/container.ts | 13 +--- src/controllers/index.ts | 1 - src/controllers/password.controller.ts | 48 -------------- src/interfaces/adapters/hash.adapter.ts | 18 ------ src/interfaces/index.ts | 4 -- src/interfaces/models/verification-code.ts | 1 - .../repositories/user.repository.ts | 5 -- .../recovery-password-verify.service.ts | 7 -- .../services/recovery-password.service.ts | 7 -- .../services/reset-password.service.ts | 9 --- src/repositories/user.repository.ts | 53 +-------------- src/routers/app.router.ts | 6 -- src/routers/index.ts | 1 - src/routers/password.router.ts | 40 ------------ src/services/index.ts | 2 - src/services/login.service.ts | 4 +- .../recovery-password-verify.service.ts | 36 ----------- src/services/recovery-password.service.ts | 64 ------------------- src/services/refresh-login.service.ts | 6 +- 22 files changed, 9 insertions(+), 390 deletions(-) delete mode 100644 src/adapters/hash/crypto.provider.ts delete mode 100644 src/adapters/hash/index.ts delete mode 100644 src/controllers/password.controller.ts delete mode 100644 src/interfaces/adapters/hash.adapter.ts delete mode 100644 src/interfaces/services/recovery-password-verify.service.ts delete mode 100644 src/interfaces/services/recovery-password.service.ts delete mode 100644 src/interfaces/services/reset-password.service.ts delete mode 100644 src/routers/password.router.ts delete mode 100644 src/services/recovery-password-verify.service.ts delete mode 100644 src/services/recovery-password.service.ts diff --git a/src/adapters/hash/crypto.provider.ts b/src/adapters/hash/crypto.provider.ts deleted file mode 100644 index 09f056d..0000000 --- a/src/adapters/hash/crypto.provider.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'; - -import type { - HashAdapterCompareDto, - HashAdapterHashDto, - HashAdapter as HashAdapterInterface, -} from '@/interfaces'; - -export class CryptoProvider implements HashAdapterInterface { - public generateSalt(length = 8): string { - return randomBytes(length).toString('hex'); - } - - public hash({ salt, text }: HashAdapterHashDto): string { - return createHash('sha512') - .update(salt + text) - .digest('hex'); - } - - public compare({ - decrypted, - encrypted, - salt, - }: HashAdapterCompareDto): boolean { - const hashToCompare = createHash('sha512') - .update(salt + decrypted) - .digest('hex'); - - // Compare the two hashes securely using timingSafeEqual to avoid timing attacks - return timingSafeEqual( - Buffer.from(encrypted, 'hex'), - Buffer.from(hashToCompare, 'hex'), - ); - } -} diff --git a/src/adapters/hash/index.ts b/src/adapters/hash/index.ts deleted file mode 100644 index 1714083..0000000 --- a/src/adapters/hash/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { inject, injectable } from 'tsyringe'; - -import { CryptoProvider } from './crypto.provider'; -import type { EnvConfig } from '@/configs'; -import { HashProviderEnum } from '@/constants'; -import type { - HashAdapterCompareDto, - HashAdapterHashDto, - HashAdapter as HashAdapterInterface, -} from '@/interfaces'; - -const providers = { - [HashProviderEnum.Crypto]: CryptoProvider, -}; - -@injectable() -export class HashAdapter implements HashAdapterInterface { - private readonly provider: HashAdapterInterface; - - constructor( - @inject('EnvConfig') - private readonly envConfig: EnvConfig, - ) { - this.provider = new providers[this.envConfig.HASH_PROVIDER](); - } - - public generateSalt(length?: number): string { - return this.provider.generateSalt(length); - } - - public hash(dto: HashAdapterHashDto): string { - return this.provider.hash(dto); - } - - public compare(dto: HashAdapterCompareDto): boolean { - return this.provider.compare(dto); - } -} diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 4abbf5f..4c72345 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -1,4 +1,3 @@ export * from './unique-id'; export * from './logger'; export * from './email'; -export * from './hash'; diff --git a/src/container.ts b/src/container.ts index 85c0448..925c180 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,18 +1,16 @@ import { container } from 'tsyringe'; -import { EmailAdapter, HashAdapter, LoggerAdapter, UniqueIdAdapter } from './adapters'; +import { EmailAdapter, LoggerAdapter, UniqueIdAdapter } from './adapters'; import { DynamoConfig, EnvConfig, JwtConfig, RateLimit } from './configs'; -import { AuthController, PasswordController, RegistrationController } from './controllers'; +import { AuthController, RegistrationController } from './controllers'; import { AuthHelper } from './helpers'; import { EnsureAuthenticatedMiddleware, ErrorHandlingMiddleware, RequestAuditMiddleware } from './middlewares'; import { EmailTemplateRepository, SessionRepository, UserRepository, VerificationCodeRepository } from './repositories'; -import { AppRouter, AuthRouter, PasswordRouter, RegistrationRouter } from './routers'; +import { AppRouter, AuthRouter, RegistrationRouter } from './routers'; import { LoginConfirmService, LoginService, LogoutService, - RecoveryPasswordService, - RecoveryPasswordVerifyService, RefreshLoginService, RegistrationConfirmService, RegistrationService, @@ -23,7 +21,6 @@ import { container.registerSingleton('UniqueIdAdapter', UniqueIdAdapter); container.registerSingleton('LoggerAdapter', LoggerAdapter); container.registerSingleton('EmailAdapter', EmailAdapter); -container.registerSingleton('HashAdapter', HashAdapter); // Configs container.registerSingleton('DynamoConfig', DynamoConfig); @@ -46,9 +43,7 @@ container.registerSingleton('SessionRepository', SessionRepos container.registerSingleton('UserRepository', UserRepository); // Services -container.registerSingleton('RecoveryPasswordVerifyService', RecoveryPasswordVerifyService); container.registerSingleton('RegistrationConfirmService', RegistrationConfirmService); -container.registerSingleton('RecoveryPasswordService', RecoveryPasswordService); container.registerSingleton('ResetPasswordService', ResetPasswordService); container.registerSingleton('RegistrationService', RegistrationService); container.registerSingleton('RefreshLoginService', RefreshLoginService); @@ -58,11 +53,9 @@ container.registerSingleton('LoginService', LoginService); // Controllers container.registerSingleton('RegistrationController', RegistrationController); -container.registerSingleton('PasswordController', PasswordController); container.registerSingleton('AuthController', AuthController); // Routers container.registerSingleton('RegistrationRouter', RegistrationRouter); -container.registerSingleton('PasswordRouter', PasswordRouter); container.registerSingleton('AuthRouter', AuthRouter); container.registerSingleton('AppRouter', AppRouter); diff --git a/src/controllers/index.ts b/src/controllers/index.ts index aca70fb..111ce32 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,2 @@ export * from './registration.controller'; -export * from './password.controller'; export * from './auth.controller'; diff --git a/src/controllers/password.controller.ts b/src/controllers/password.controller.ts deleted file mode 100644 index 6f80897..0000000 --- a/src/controllers/password.controller.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { FastifyReply, FastifyRequest } from 'fastify'; -import { inject, injectable } from 'tsyringe'; - -import { HttpStatusCodesEnum } from '@/constants'; -import type { - RecoveryPasswordService, - RecoveryPasswordVerifyService, - ResetPasswordService, - ResetPasswordServiceDto, -} from '@/interfaces'; - -@injectable() -export class PasswordController { - 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 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 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/adapters/hash.adapter.ts b/src/interfaces/adapters/hash.adapter.ts deleted file mode 100644 index 4afea06..0000000 --- a/src/interfaces/adapters/hash.adapter.ts +++ /dev/null @@ -1,18 +0,0 @@ -export type HashAdapterHashDto = { - text: string; - salt: string; -}; - -export type HashAdapterCompareDto = { - decrypted: string; - encrypted: string; - salt: string; -}; - -export interface HashAdapter { - generateSalt(length?: number): string; - - hash(dto: HashAdapterHashDto): string; - - compare(dto: HashAdapterCompareDto): boolean; -} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index a1ed75d..5eda99f 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -2,7 +2,6 @@ export * from './adapters/unique-id.adapter'; export * from './adapters/logger.adapter'; export * from './adapters/email.adapter'; -export * from './adapters/hash.adapter'; // Helpers export * from './helpers/auth.helper'; @@ -28,10 +27,7 @@ export * from './repositories/user.repository'; export * from './routers/router'; // Services -export * from './services/recovery-password-verify.service'; export * from './services/registration-confirm.service'; -export * from './services/recovery-password.service'; -export * from './services/reset-password.service'; export * from './services/login-confirm.service'; export * from './services/refresh-login.service'; export * from './services/registration.service'; diff --git a/src/interfaces/models/verification-code.ts b/src/interfaces/models/verification-code.ts index 6594ade..59214a4 100644 --- a/src/interfaces/models/verification-code.ts +++ b/src/interfaces/models/verification-code.ts @@ -1,5 +1,4 @@ export enum VerificationCodeTypeEnum { - RecoveryPassword = 'recovery-password', Registration = 'registration', Login = 'login', } diff --git a/src/interfaces/repositories/user.repository.ts b/src/interfaces/repositories/user.repository.ts index 3e9a4cf..b47cd6e 100644 --- a/src/interfaces/repositories/user.repository.ts +++ b/src/interfaces/repositories/user.repository.ts @@ -4,9 +4,4 @@ export interface UserRepository { create(user: Omit): Promise; findByEmail(dto: { email: string }): Promise; - - updateByEmail(dto: { - email: string; - update: Partial>; - }): Promise; } diff --git a/src/interfaces/services/recovery-password-verify.service.ts b/src/interfaces/services/recovery-password-verify.service.ts deleted file mode 100644 index f270f1a..0000000 --- a/src/interfaces/services/recovery-password-verify.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type RecoveryPasswordVerifyServiceDto = { - code: string; -}; - -export interface RecoveryPasswordVerifyService { - execute(dto: RecoveryPasswordVerifyServiceDto): Promise; -} diff --git a/src/interfaces/services/recovery-password.service.ts b/src/interfaces/services/recovery-password.service.ts deleted file mode 100644 index 025d81f..0000000 --- a/src/interfaces/services/recovery-password.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type RecoveryPasswordServiceDto = { - email: string; -}; - -export interface RecoveryPasswordService { - execute(dto: RecoveryPasswordServiceDto): Promise; -} diff --git a/src/interfaces/services/reset-password.service.ts b/src/interfaces/services/reset-password.service.ts deleted file mode 100644 index 170b2bf..0000000 --- a/src/interfaces/services/reset-password.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type ResetPasswordServiceDto = { - password: string; - email: string; - code: string; -}; - -export interface ResetPasswordService { - execute(dto: ResetPasswordServiceDto): Promise; -} diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index 798dda8..acf463b 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -3,8 +3,6 @@ import { 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'; @@ -25,7 +23,7 @@ import type { - PK: user - SK: id:{id} - ReferenceId: [reference_type]:{reference_value} - - Content: { name, email, password, id, created_at } + - Content: { name, email, id, created_at } - TTL: INT */ @@ -105,53 +103,4 @@ export class UserRepository implements UserRepositoryInterface { throw error; } } - - public async updateByEmail({ - update, - email, - }: { - update: Partial>; - email: string; - }): Promise { - try { - const user = await this.findByEmail({ email }); - - if (!user) { - return this.logger.warn('User not found for update:', email); - } - - const updateExpressions: string[] = []; - const expressionAttributeValues: Record = {}; - const expressionAttributeNames: Record = {}; - - for (const [key, value] of Object.entries(update)) { - const attributeName = `#${key}`; - const attributeValue = `:${key}`; - - updateExpressions.push(`Content.${attributeName} = ${attributeValue}`); - expressionAttributeValues[attributeValue] = value; - expressionAttributeNames[attributeName] = key; - } - - 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/routers/app.router.ts b/src/routers/app.router.ts index cb7fce9..7e576d9 100644 --- a/src/routers/app.router.ts +++ b/src/routers/app.router.ts @@ -9,8 +9,6 @@ export class AppRouter implements Router { constructor( @inject('RegistrationRouter') private readonly registrationRouter: Router, - @inject('PasswordRouter') private readonly passwordRouter: Router, - @inject('AuthRouter') private readonly authRouter: Router, ) {} @@ -31,10 +29,6 @@ export class AppRouter implements Router { prefix: '/v1/auth', }); - app.register(this.passwordRouter.routes.bind(this.passwordRouter), { - prefix: '/v1/password', - }); - if (done) { done(); } diff --git a/src/routers/index.ts b/src/routers/index.ts index 68a92f3..0eba590 100644 --- a/src/routers/index.ts +++ b/src/routers/index.ts @@ -1,4 +1,3 @@ export * from './registration.router'; -export * from './password.router'; export * from './auth.router'; export * from './app.router'; diff --git a/src/routers/password.router.ts b/src/routers/password.router.ts deleted file mode 100644 index 324412e..0000000 --- a/src/routers/password.router.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { FastifyInstance } from 'fastify'; -import { inject, injectable } from 'tsyringe'; - -import type { PasswordController } from '@/controllers'; -import type { Router } from '@/interfaces'; - -@injectable() -export class PasswordRouter implements Router { - constructor( - @inject('PasswordController') - private readonly passwordController: PasswordController, - ) {} - - public routes( - app: FastifyInstance, - _?: unknown, - done?: (err?: Error) => void, - ): FastifyInstance { - app.post( - '/recovery', - this.passwordController.recovery.bind(this.passwordController), - ); - - app.post( - '/recovery/verify', - this.passwordController.recoveryVerify.bind(this.passwordController), - ); - - app.post( - '/reset', - this.passwordController.reset.bind(this.passwordController), - ); - - if (done) { - done(); - } - - return app; - } -} diff --git a/src/services/index.ts b/src/services/index.ts index 9a8f794..22d8744 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,6 +1,4 @@ -export * from './recovery-password-verify.service'; export * from './registration-confirm.service'; -export * from './recovery-password.service'; export * from './reset-password.service'; export * from './refresh-login.service'; export * from './login-confirm.service'; diff --git a/src/services/login.service.ts b/src/services/login.service.ts index 2647768..bb4fcf8 100644 --- a/src/services/login.service.ts +++ b/src/services/login.service.ts @@ -40,13 +40,13 @@ export class LoginService implements LoginServiceInterface { const verificationCodeExists = await this.verificationCodeRepository.findOneByContent({ - code_type: VerificationCodeTypeEnum.Registration, + code_type: VerificationCodeTypeEnum.Login, content: { key: 'email', value: email }, }); if (verificationCodeExists) { await this.verificationCodeRepository.deleteOne({ - code_type: VerificationCodeTypeEnum.Registration, + code_type: VerificationCodeTypeEnum.Login, token: verificationCodeExists.token, }); } diff --git a/src/services/recovery-password-verify.service.ts b/src/services/recovery-password-verify.service.ts deleted file mode 100644 index 21df8fd..0000000 --- a/src/services/recovery-password-verify.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { inject, injectable } from 'tsyringe'; - -import { AppErrorCodeEnum, HttpStatusCodesEnum } from '@/constants'; -import { AppError } from '@/errors'; -import { - type RecoveryPasswordVerifyServiceDto, - type RecoveryPasswordVerifyService as RecoveryPasswordVerifyServiceInterface, - type VerificationCodeRepository, - VerificationCodeTypeEnum, -} from '@/interfaces'; - -@injectable() -export class RecoveryPasswordVerifyService - implements RecoveryPasswordVerifyServiceInterface -{ - constructor( - @inject('VerificationCodeRepository') - private readonly verificationCodeRepository: VerificationCodeRepository, - ) {} - - public async execute({ - code, - }: RecoveryPasswordVerifyServiceDto): Promise { - const verificationCode = await this.verificationCodeRepository.findOne({ - code_type: VerificationCodeTypeEnum.RecoveryPassword, - code, - }); - - if (!verificationCode) { - throw new AppError({ - message: AppErrorCodeEnum.VerificationCodeNotFound, - status_code: HttpStatusCodesEnum.NOT_FOUND, - }); - } - } -} diff --git a/src/services/recovery-password.service.ts b/src/services/recovery-password.service.ts deleted file mode 100644 index 7f557c7..0000000 --- a/src/services/recovery-password.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { inject, injectable } from 'tsyringe'; - -import { - type EmailAdapter, - type RecoveryPasswordServiceDto, - type RecoveryPasswordService as RecoveryPasswordServiceInterface, - type UserRepository, - type VerificationCodeRepository, - VerificationCodeTypeEnum, -} from '@/interfaces'; - -@injectable() -export class RecoveryPasswordService - implements RecoveryPasswordServiceInterface -{ - constructor( - @inject('UserRepository') - private readonly userRepository: UserRepository, - - @inject('VerificationCodeRepository') - private readonly verificationCodeRepository: VerificationCodeRepository, - - @inject('EmailAdapter') - private readonly emailAdapter: EmailAdapter, - ) {} - - public async execute({ email }: RecoveryPasswordServiceDto): Promise { - const user = await this.userRepository.findByEmail({ email }); - - if (!user) { - return; - } - - const verificationCodeExists = - await this.verificationCodeRepository.findOneByContent({ - code_type: VerificationCodeTypeEnum.RecoveryPassword, - content: { key: 'email', value: email }, - }); - - if (verificationCodeExists) { - await this.verificationCodeRepository.deleteOne({ - code: verificationCodeExists.code, - code_type: VerificationCodeTypeEnum.RecoveryPassword, - }); - } - - const expirationTimeInMinutes = 10; - const expiresAt = new Date( - Date.now() + expirationTimeInMinutes * 60 * 1000, - ).toISOString(); - - const verificationCode = await this.verificationCodeRepository.create({ - code_type: VerificationCodeTypeEnum.RecoveryPassword, - code_expires_at: expiresAt, - content: { email }, - }); - - await this.emailAdapter.send({ - to: email, - subject: 'Recovery password', - html: `${verificationCode}`, - }); - } -} diff --git a/src/services/refresh-login.service.ts b/src/services/refresh-login.service.ts index 49b2a4d..8cb3a1e 100644 --- a/src/services/refresh-login.service.ts +++ b/src/services/refresh-login.service.ts @@ -57,9 +57,9 @@ export class RefreshLoginService implements RefreshLoginServiceInterface { private getUserId({ refreshToken }: { refreshToken: string }): string { try { - const decodedToken = this.jwtConfig.verify<{ sub?: string }>( - refreshToken, - ); + const decodedToken = this.jwtConfig.verify<{ sub?: string }>({ + token: refreshToken, + }); if (!decodedToken?.sub) { throw new AppError({