diff --git a/README.md b/README.md index d1a0b0c..ae095e0 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,6 @@ This is where i'm going to place my ideas and things that i want to do or use in - [x] Add base user roles such as Admin, Student and Instructor - [x] Environment service - [x] Add JWT Secret -- [ ] Translate Auth error messages -- [ ] Translate role guard error messages +- [x] Translate Auth error messages +- [x] Translate role guard error messages - [x] SWC diff --git a/src/generated/i18n.generated.ts b/src/generated/i18n.generated.ts index e8cbdcc..3dba83d 100644 --- a/src/generated/i18n.generated.ts +++ b/src/generated/i18n.generated.ts @@ -1,58 +1,59 @@ /* DO NOT EDIT, file generated by nestjs-i18n */ -import { Path } from "nestjs-i18n"; +import { Path } from 'nestjs-i18n'; export type I18nTranslations = { - "auth": { - "errors": { - "user-not-found": string; - "incorrect-password": string; - }; - "validations": { - "EMAIL_IS_DEFINED": string; - "EMAIL_IS_EMAIL": string; - "PASSWORD_IS_DEFINED": string; - "PASSWORD_IS_STRING": string; - }; + auth: { + validations: { + EMAIL_IS_DEFINED: string; + EMAIL_IS_EMAIL: string; + PASSWORD_IS_DEFINED: string; + PASSWORD_IS_STRING: string; }; - "course": { - "validations": { - "TITLE_IS_DEFINED": string; - "TITLE_IS_STRING": string; - "TITLE_IS_NOT_EMPTY": string; - "DESCRIPTION_IS_DEFINED": string; - "DESCRIPTION_IS_STRING": string; - "DESCRIPTION_IS_NOT_EMPTY": string; - "PRICE_IS_DEFINED": string; - "PRICE_IS_NUMBER": string; - "PRICE_MIN": string; - "INSTRUCTOR_IS_DEFINED": string; - "INSTRUCTOR_IS_UUID": string; - "STUDENT_IS_DEFINED": string; - "STUDENT_IS_UUID": string; - }; - "errors": { - "invalid-money": string; - "instructor-not-found": string; - "course-not-found": string; - "student-not-found": string; - "student-already-enrolled": string; - }; + errors: { + 'user-not-found': string; + 'incorrect-password': string; + 'no-authorization': string; }; - "user": { - "validations": { - "EMAIL_IS_EMAIL": string; - "NAME_IS_DEFINED": string; - "NAME_IS_STRING": string; - "NAME_IS_NOT_EMPTY": string; - "PASSWORD_IS_DEFINED": string; - "PASSWORD_IS_STRING": string; - "PASSWORD_IS_NOT_EMPTY": string; - }; - "errors": { - "duplicated-email": string; - "invalid-email": string; - "invalid-name": string; - }; + }; + course: { + validations: { + TITLE_IS_DEFINED: string; + TITLE_IS_STRING: string; + TITLE_IS_NOT_EMPTY: string; + DESCRIPTION_IS_DEFINED: string; + DESCRIPTION_IS_STRING: string; + DESCRIPTION_IS_NOT_EMPTY: string; + PRICE_IS_DEFINED: string; + PRICE_IS_NUMBER: string; + PRICE_MIN: string; + INSTRUCTOR_IS_DEFINED: string; + INSTRUCTOR_IS_UUID: string; + STUDENT_IS_DEFINED: string; + STUDENT_IS_UUID: string; }; + errors: { + 'invalid-money': string; + 'instructor-not-found': string; + 'course-not-found': string; + 'student-not-found': string; + 'student-already-enrolled': string; + }; + }; + user: { + validations: { + EMAIL_IS_EMAIL: string; + NAME_IS_DEFINED: string; + NAME_IS_STRING: string; + NAME_IS_NOT_EMPTY: string; + PASSWORD_IS_DEFINED: string; + PASSWORD_IS_STRING: string; + PASSWORD_IS_NOT_EMPTY: string; + }; + errors: { + 'duplicated-email': string; + 'invalid-email': string; + 'invalid-name': string; + }; + }; }; export type I18nPath = Path; diff --git a/src/i18n/en/auth.json b/src/i18n/en/auth.json index 196863d..2b33f90 100644 --- a/src/i18n/en/auth.json +++ b/src/i18n/en/auth.json @@ -1,12 +1,13 @@ { - "errors": { - "user-not-found": "Could not find a user with the provided e-mail.", - "incorrect-password": "The provided password is incorrect." - }, "validations": { "EMAIL_IS_DEFINED": "You need to send the e-mail to authenticate.", "EMAIL_IS_EMAIL": "You need to send a valid e-mail to authenticate.", "PASSWORD_IS_DEFINED": "You need to send your password to authenticate.", "PASSWORD_IS_STRING": "You need to send a valid password to authenticate." + }, + "errors": { + "user-not-found": "Could not find a user with the provided e-mail.", + "incorrect-password": "The provided password is incorrect.", + "no-authorization": "You are not authorized to access this resource." } } diff --git a/src/i18n/pt-br/auth.json b/src/i18n/pt-br/auth.json index 28a9ba1..734ea44 100644 --- a/src/i18n/pt-br/auth.json +++ b/src/i18n/pt-br/auth.json @@ -1,12 +1,13 @@ { - "errors": { - "user-not-found": "Não foi possível encontrar um usuário com o e-mail enviado.", - "incorrect-password": "Senha incorreta." - }, "validations": { "EMAIL_IS_DEFINED": "É necessário enviar o e-mail para realizar a autenticação.", "EMAIL_IS_EMAIL": "É necessário enviar um e-mail válido.", "PASSWORD_IS_DEFINED": "É necessário enviar a senha para realizar a autenticação.", "PASSWORD_IS_STRING": "É necessário enviar uma senha válida." + }, + "errors": { + "user-not-found": "Não foi possível encontrar um usuário com o e-mail enviado.", + "incorrect-password": "Senha incorreta.", + "no-authorization": "Você não é autorizado para acessar este recurso." } } diff --git a/src/i18n/pt-br/user.json b/src/i18n/pt-br/user.json index 41fe638..d273f9c 100644 --- a/src/i18n/pt-br/user.json +++ b/src/i18n/pt-br/user.json @@ -13,4 +13,4 @@ "invalid-email": "O e-mail enviado está inválido.", "invalid-name": "O nome enviado está inválido." } -} \ No newline at end of file +} diff --git a/src/modules/auth/infra/guards/auth-jwt.guard.ts b/src/modules/auth/infra/guards/auth-jwt.guard.ts index 1f6a2af..0dd35bd 100644 --- a/src/modules/auth/infra/guards/auth-jwt.guard.ts +++ b/src/modules/auth/infra/guards/auth-jwt.guard.ts @@ -1,5 +1,21 @@ -import { Injectable } from '@nestjs/common'; +import { RequestUserEntity } from '@modules/auth/domain/entities/request-user.entity'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { I18nContext } from 'nestjs-i18n'; +import { I18nTranslations } from 'src/generated/i18n.generated'; @Injectable() -export class AuthJwtGuard extends AuthGuard('jwt') {} +export class AuthJwtGuard extends AuthGuard('jwt') { + handleRequest(err: Error, user: T | false): T { + const i18n = I18nContext.current(); + + if (err || !user) { + throw ( + err || + new UnauthorizedException(i18n?.t('auth.errors.no-authorization')) + ); + } + + return user; + } +} diff --git a/src/modules/auth/infra/strategies/auth-local.strategy.ts b/src/modules/auth/infra/strategies/auth-local.strategy.ts index c9bfba2..6fb1bdd 100644 --- a/src/modules/auth/infra/strategies/auth-local.strategy.ts +++ b/src/modules/auth/infra/strategies/auth-local.strategy.ts @@ -8,17 +8,14 @@ import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { LoginUseCase } from '@modules/auth/domain/usecases/login.usecase'; import { UserNotFoundError } from '@modules/auth/domain/errors/user-not-found.error'; -import { I18nService } from 'nestjs-i18n'; +import { I18nContext } from 'nestjs-i18n'; import { I18nTranslations } from 'src/generated/i18n.generated'; import { IncorrectPasswordError } from '@modules/auth/domain/errors/incorrect-password.error'; import { UserEntity } from '@modules/user/domain/entities/user/user.entity'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { - constructor( - private readonly loginUseCase: LoginUseCase, - private readonly i18n: I18nService, - ) { + constructor(private readonly loginUseCase: LoginUseCase) { super({ usernameField: 'email', }); @@ -34,13 +31,15 @@ export class LocalStrategy extends PassportStrategy(Strategy) { return result.value.user; } + const i18n = I18nContext.current(); + if (result.value instanceof UserNotFoundError) { - throw new NotFoundException(this.i18n.t('auth.errors.user-not-found')); + throw new NotFoundException(i18n?.t('auth.errors.user-not-found')); } if (result.value instanceof IncorrectPasswordError) { throw new UnauthorizedException( - this.i18n.t('auth.errors.incorrect-password'), + i18n?.t('auth.errors.incorrect-password'), ); } diff --git a/src/modules/course/presenter/controllers/course.controller.ts b/src/modules/course/presenter/controllers/course.controller.ts index ac7bbba..3f6e1c9 100644 --- a/src/modules/course/presenter/controllers/course.controller.ts +++ b/src/modules/course/presenter/controllers/course.controller.ts @@ -51,7 +51,7 @@ export class CourseController { ) {} @Get() - @ProtectedTo(UserRole.ADMIN, UserRole.INSTRUCTOR, UserRole.STUDENT) + @ProtectedTo(UserRole.INSTRUCTOR) @ApiOperation({ summary: 'Get all courses (paginated)' }) @ApiHeader({ name: 'Accept-Language', example: 'en', required: false }) @ApiQuery({ @@ -84,8 +84,7 @@ export class CourseController { } @Post() - @UseGuards(AuthJwtGuard) - @ApiBearerAuth() + @ProtectedTo(UserRole.ADMIN, UserRole.INSTRUCTOR, UserRole.STUDENT) @ApiOperation({ summary: 'Creates a new course' }) @ApiHeader({ name: 'Accept-Language', example: 'en', required: false }) @ApiBody({ diff --git a/src/shared/presenter/decorators/protected-to.decorator.ts b/src/shared/presenter/decorators/protected-to.decorator.ts index a6e59b8..0907c9c 100644 --- a/src/shared/presenter/decorators/protected-to.decorator.ts +++ b/src/shared/presenter/decorators/protected-to.decorator.ts @@ -17,6 +17,6 @@ export const ProtectedTo = (...roles: UserRole[]) => ApiForbiddenResponse({ type: ForbiddenException, status: 403, - description: 'Você não possui autorização para acessar esse recurso.', + description: 'You are not authorized to access this resource.', }), ); diff --git a/src/shared/presenter/guards/role.guard.ts b/src/shared/presenter/guards/role.guard.ts index 868fddc..e10edd0 100644 --- a/src/shared/presenter/guards/role.guard.ts +++ b/src/shared/presenter/guards/role.guard.ts @@ -7,10 +7,12 @@ import { Injectable, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { I18nContext } from 'nestjs-i18n'; +import { I18nTranslations } from 'src/generated/i18n.generated'; @Injectable() export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} + constructor(private readonly reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const roles = this.reflector.get('roles', context.getHandler()); @@ -22,16 +24,14 @@ export class RolesGuard implements CanActivate { const request = context.switchToHttp().getRequest(); const user = request.user as RequestUserEntity; + const i18n = I18nContext.current(); + if (!user || !user.roles) { - throw new ForbiddenException( - 'Você não possui autorização para acessar esse recurso.', - ); + throw new ForbiddenException(i18n?.t('auth.errors.no-authorization')); } if (!roles.some((role) => user.roles.includes(role))) { - throw new ForbiddenException( - 'Você não possui autorização para acessar esse recurso.', - ); + throw new ForbiddenException(i18n?.t('auth.errors.no-authorization')); } return true;