From f6ccce0b08602959bcba6b6d84d0e145cc46bb09 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:07:40 -0300 Subject: [PATCH 1/9] (#136) adiciona novas variaveis de ambiente --- .env.development | 4 +++- .env.test | 4 +++- docker-compose.test.yml | 1 + docker-compose.yml | 5 +++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.env.development b/.env.development index 1d054b2..ef40318 100644 --- a/.env.development +++ b/.env.development @@ -6,6 +6,8 @@ DB_PASS=postgres DB_DATABASE=gerocuidado-usuario-db DB_PORT=5001 -#BCRYPT +#JWT TOKEN +JWT_TOKEN_SECRET=f57d8cc37a35a8051aa97b5ec8506a2ac479e81f82aed9de975a0cb90b903044 +JWT_TOKEN_EXPIRES_IN=12h HASH_SALT=10 diff --git a/.env.test b/.env.test index fe56cc4..553476f 100644 --- a/.env.test +++ b/.env.test @@ -6,5 +6,7 @@ DB_PASS=postgres DB_DATABASE=gerocuidado-usuario-db-test DB_PORT=5001 -#BCRYPT +#JWT TOKEN +JWT_TOKEN_SECRET=f57d8cc37a35a8051aa97b5ec8506a2ac479e81f82aed9de975a0cb90b903044 +JWT_TOKEN_EXPIRES_IN=12h HASH_SALT=10 diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 35c777e..c0da309 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -12,6 +12,7 @@ services: user: root ports: - '3001:3001' + - '8001:8001' depends_on: - gerocuidado-usuario-db-test networks: diff --git a/docker-compose.yml b/docker-compose.yml index fba6e43..eaee744 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,13 @@ services: - NODE_ENV=development ports: - '3001:3001' + - '4001:4001' - '7001:9001' depends_on: - gerocuidado-usuario-db networks: - gerocuidado-usuario-net + - gerocuidado-apis-net gerocuidado-usuario-db: build: @@ -36,3 +38,6 @@ services: networks: gerocuidado-usuario-net: driver: bridge + gerocuidado-apis-net: + name: gerocuidado-apis-net + external: true From 6986d5871464920259f0f370fcc8709216c45155 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:08:22 -0300 Subject: [PATCH 2/9] (#136) adiciona microsservico tcp --- src/main.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main.ts b/src/main.ts index 2185e16..bfe5f60 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import compression from 'compression'; import { json, urlencoded } from 'express'; import helmet from 'helmet'; +import { Transport } from '@nestjs/microservices'; import { AppModule } from './app.module'; import { AllExceptionsFilter } from './shared/filters/all-exceptions.filter'; import { ModelNotFoundExceptionFilter } from './shared/filters/model-not-found.exception-filter'; @@ -34,6 +35,16 @@ async function bootstrap() { app.enableCors(); + app.connectMicroservice({ + transport: Transport.TCP, + options: { + // TODO analisar em prod esse host + host: '0.0.0.0', + port: 4001, + }, + }); + + await app.startAllMicroservices(); await app.listen(3001); console.log(`Application is running on: ${await app.getUrl()}`); } From b82eca1bff471302b29eb8921a4d50ba5603234a Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:08:36 -0300 Subject: [PATCH 3/9] (#136) altera busca por email do usuario --- src/usuario/usuario.service.spec.ts | 20 ++++++++++++++++---- src/usuario/usuario.service.ts | 16 +++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/usuario/usuario.service.spec.ts b/src/usuario/usuario.service.spec.ts index 2f3a1e0..02f3985 100644 --- a/src/usuario/usuario.service.spec.ts +++ b/src/usuario/usuario.service.spec.ts @@ -62,7 +62,13 @@ describe('UsuarioService', () => { it('should create Usuario', async () => { const user = { nome: 'Henrique' } as any; jest.spyOn(repository, 'save').mockReturnValue({ id: 1 } as any); - jest.spyOn(repository, 'findOne').mockReturnValue(undefined as any); + jest.spyOn(repository, 'createQueryBuilder').mockReturnValue({ + where: () => ({ + addSelect: () => ({ + getOne: jest.fn().mockResolvedValueOnce(undefined), + }), + }), + } as any); jest.spyOn(configService, 'get').mockReturnValue(10 as any); jest .spyOn(bcrypt, 'hash') @@ -75,9 +81,15 @@ describe('UsuarioService', () => { it('should not create Usuario', async () => { const user = { nome: 'Henrique' } as any; - jest - .spyOn(repository, 'findOne') - .mockReturnValue({ email: 'fulano@gmail.com' } as any); + jest.spyOn(repository, 'createQueryBuilder').mockReturnValue({ + where: () => ({ + addSelect: () => ({ + getOne: jest + .fn() + .mockResolvedValueOnce({ email: 'fulano@gmail.com' } as any), + }), + }), + } as any); expect(service.create(user)).rejects.toThrow( new BadRequestException('Este email já está cadastrado!'), ); diff --git a/src/usuario/usuario.service.ts b/src/usuario/usuario.service.ts index 9a34602..fdeceec 100644 --- a/src/usuario/usuario.service.ts +++ b/src/usuario/usuario.service.ts @@ -25,8 +25,10 @@ export class UsuarioService { async create(body: CreateUsuarioDto): Promise { const usuario = new Usuario(body); - await this.checkEmail(usuario.email); + + await this.checkUserExists(usuario.email); usuario.senha = await this.hashPassword(usuario.senha); + return this._repository.save(usuario); } @@ -35,14 +37,22 @@ export class UsuarioService { return bcrypt.hash(senha, Number(salt)); } - async checkEmail(email: string) { - const userFound = await this._repository.findOne({ where: { email } }); + private async checkUserExists(email: string) { + const userFound = await this.findByEmail(email); if (userFound) { throw new BadRequestException('Este email já está cadastrado!'); } } + async findByEmail(email: string): Promise { + return this._repository + .createQueryBuilder('usuario') + .where('usuario.email = :email', { email }) + .addSelect('usuario.senha') + .getOne(); + } + async findOne(id: number) { return this._repository.findOneOrFail({ where: { id } }); } From a56d8e7c6b2d96727e9c74dad79fa4feb7bc57f1 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:08:52 -0300 Subject: [PATCH 4/9] (#136) adiciona resposta para login em HTTPResponse --- src/shared/classes/http-response.spec.ts | 12 ++++++++++++ src/shared/classes/http-response.ts | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/src/shared/classes/http-response.spec.ts b/src/shared/classes/http-response.spec.ts index 1456231..2a0e0c8 100644 --- a/src/shared/classes/http-response.spec.ts +++ b/src/shared/classes/http-response.spec.ts @@ -52,4 +52,16 @@ describe('HttpResponse', () => { expect(updated).toEqual(expected); }); + + it('should create message with login text', () => { + const response = new HttpResponse({}); + const updated = response.onLogin(); + + const expected = { + message: 'Login efetuado com sucesso!', + data: {}, + }; + + expect(updated).toEqual(expected); + }); }); diff --git a/src/shared/classes/http-response.ts b/src/shared/classes/http-response.ts index f5ce99a..48edb05 100644 --- a/src/shared/classes/http-response.ts +++ b/src/shared/classes/http-response.ts @@ -28,4 +28,9 @@ export class HttpResponse implements Response { this.message = 'Excluído com sucesso!'; return this; } + + onLogin(): Response { + this.message = 'Login efetuado com sucesso!'; + return this; + } } From 6a931be4b8d2a56558a1448c956e71c5f0af0aa1 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:09:07 -0300 Subject: [PATCH 5/9] (#136) adiciona um decorator Public --- src/shared/decorators/public-route.decorator.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/shared/decorators/public-route.decorator.ts diff --git a/src/shared/decorators/public-route.decorator.ts b/src/shared/decorators/public-route.decorator.ts new file mode 100644 index 0000000..c27f73c --- /dev/null +++ b/src/shared/decorators/public-route.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const PublicRoute = () => SetMetadata(IS_PUBLIC_KEY, true); From 5c25c4f7fed0d4f388b1855fafdad4660f9cb8f4 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:10:44 -0300 Subject: [PATCH 6/9] (#136) adiciona jwt strategy e guard --- src/autenticacao/interfaces/jwt.interface.ts | 8 +++ src/autenticacao/jwt-auth.guard.spec.ts | 47 ++++++++++++++++ src/autenticacao/jwt-auth.guard.ts | 37 ++++++++++++ src/autenticacao/jwt.strategy.spec.ts | 59 ++++++++++++++++++++ src/autenticacao/jwt.strategy.ts | 20 +++++++ 5 files changed, 171 insertions(+) create mode 100644 src/autenticacao/interfaces/jwt.interface.ts create mode 100644 src/autenticacao/jwt-auth.guard.spec.ts create mode 100644 src/autenticacao/jwt-auth.guard.ts create mode 100644 src/autenticacao/jwt.strategy.spec.ts create mode 100644 src/autenticacao/jwt.strategy.ts diff --git a/src/autenticacao/interfaces/jwt.interface.ts b/src/autenticacao/interfaces/jwt.interface.ts new file mode 100644 index 0000000..b0a4b3f --- /dev/null +++ b/src/autenticacao/interfaces/jwt.interface.ts @@ -0,0 +1,8 @@ +export interface JwtPayload { + id: number; + nome: string; + email: string; + admin: boolean; + iat: number; + exp: number; +} diff --git a/src/autenticacao/jwt-auth.guard.spec.ts b/src/autenticacao/jwt-auth.guard.spec.ts new file mode 100644 index 0000000..2b6940d --- /dev/null +++ b/src/autenticacao/jwt-auth.guard.spec.ts @@ -0,0 +1,47 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Test, TestingModule } from '@nestjs/testing'; +import { JwtAuthGuard } from './jwt-auth.guard'; + +describe('JwtAuthGuard', () => { + let guard: JwtAuthGuard; + let reflector: Reflector; + + const mockReflector = { + getAllAndOverride: jest.fn(), + }; + + const context = { + getHandler: () => '', + getClass: () => '', + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { provide: Reflector, useValue: mockReflector }, + JwtAuthGuard, + ], + }).compile(); + + guard = module.get(JwtAuthGuard); + reflector = module.get(Reflector); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should activate public routes', () => { + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(true); + + expect(guard.canActivate(context as any)).toEqual(true); + }); + + it('should handle request for no user', () => { + const callHandleRequest = () => + guard.handleRequest({} as any, undefined as any, {} as any, {} as any); + + expect(callHandleRequest).toThrowError(UnauthorizedException); + }); +}); diff --git a/src/autenticacao/jwt-auth.guard.ts b/src/autenticacao/jwt-auth.guard.ts new file mode 100644 index 0000000..713b0aa --- /dev/null +++ b/src/autenticacao/jwt-auth.guard.ts @@ -0,0 +1,37 @@ +import { + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { IS_PUBLIC_KEY } from '../shared/decorators/public-route.decorator'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt-auth') { + constructor(private readonly _reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this._reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + return isPublic ? true : super.canActivate(context); + } + + handleRequest( + err: unknown, + user: JwtPayload, + info: unknown, + context: ExecutionContext, + ) { + if (!user) { + throw new UnauthorizedException('Usuário não autenticado!'); + } + + return super.handleRequest(err, user, info, context); + } +} diff --git a/src/autenticacao/jwt.strategy.spec.ts b/src/autenticacao/jwt.strategy.spec.ts new file mode 100644 index 0000000..bbd4310 --- /dev/null +++ b/src/autenticacao/jwt.strategy.spec.ts @@ -0,0 +1,59 @@ +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { JwtPayload } from './interfaces/jwt.interface'; +import { JwtStrategy } from './jwt.strategy'; + +class PublicJwtStrategy extends JwtStrategy { + public validate(payload: any): Promise { + return super.validate(payload); + } +} + +describe('JwtStrategy', () => { + let strategy: JwtStrategy; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ConfigService, + { + provide: JwtStrategy, + useValue: { + constructor: jest.fn(), + validate: jest.fn(), + }, + }, + ], + }).compile(); + strategy = module.get(JwtStrategy); + configService = module.get(ConfigService); + }); + + it('should be defined', () => { + expect(strategy).toBeDefined(); + expect(configService).toBeDefined(); + }); + + it('should validate', async () => { + const spy = jest + .spyOn(configService, 'get') + .mockReturnValue( + 'a0e47b13e1f3984172bf162aa2832ae6da4333c1881728d88c3792b29463d459', + ); + + const publicJwtStrategy = new PublicJwtStrategy(configService); + const payload: JwtPayload = { + exp: 0, + iat: 0, + id: 1, + nome: 'zé', + email: 'ze@email.com', + admin: false, + }; + const returned = { ...payload }; + + expect(await publicJwtStrategy.validate(payload)).toEqual(returned); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/autenticacao/jwt.strategy.ts b/src/autenticacao/jwt.strategy.ts new file mode 100644 index 0000000..f2e01a6 --- /dev/null +++ b/src/autenticacao/jwt.strategy.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtPayload } from './interfaces/jwt.interface'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-auth') { + constructor(protected readonly configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_TOKEN_SECRET'), + }); + } + + protected async validate(payload: JwtPayload) { + return { ...payload }; + } +} From 7a89bc2784b0a5d8354bdf3c32dbf51a8a8700a5 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:11:07 -0300 Subject: [PATCH 7/9] (#136) adiciona autenticacao controller e service --- .../autenticacao.controller.spec.ts | 57 ++++++++++++ src/autenticacao/autenticacao.controller.ts | 29 +++++++ src/autenticacao/autenticacao.module.ts | 26 ++++++ src/autenticacao/autenticacao.service.spec.ts | 87 +++++++++++++++++++ src/autenticacao/autenticacao.service.ts | 53 +++++++++++ src/autenticacao/dto/login.dto.ts | 12 +++ 6 files changed, 264 insertions(+) create mode 100644 src/autenticacao/autenticacao.controller.spec.ts create mode 100644 src/autenticacao/autenticacao.controller.ts create mode 100644 src/autenticacao/autenticacao.module.ts create mode 100644 src/autenticacao/autenticacao.service.spec.ts create mode 100644 src/autenticacao/autenticacao.service.ts create mode 100644 src/autenticacao/dto/login.dto.ts diff --git a/src/autenticacao/autenticacao.controller.spec.ts b/src/autenticacao/autenticacao.controller.spec.ts new file mode 100644 index 0000000..39ac2f6 --- /dev/null +++ b/src/autenticacao/autenticacao.controller.spec.ts @@ -0,0 +1,57 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpResponse } from '../shared/classes/http-response'; +import { AutenticacaoController } from './autenticacao.controller'; +import { AutenticacaoService } from './autenticacao.service'; + +describe('AutenticacaoController', () => { + let controller: AutenticacaoController; + let service: AutenticacaoService; + + const mockAutenticacaoService = { + login: jest.fn(), + validateToken: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + controllers: [AutenticacaoController], + providers: [ + { + provide: AutenticacaoService, + useValue: mockAutenticacaoService, + }, + ], + }).compile(); + + controller = module.get(AutenticacaoController); + service = module.get(AutenticacaoService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + expect(service).toBeDefined(); + }); + + it('should login', async () => { + jest.spyOn(service, 'login').mockReturnValue(Promise.resolve('token')); + + expect(await controller.login({ email: 'a', senha: 'a' })).toEqual( + new HttpResponse('token').onLogin(), + ); + }); + + it('should validateToken', async () => { + jest.spyOn(service, 'validateToken').mockReturnValue(true); + + expect(await controller.validateToken({ jwt: 'token' })).toEqual(true); + }); + + it('should not validateToken', async () => { + jest.spyOn(service, 'validateToken').mockImplementation(() => { + throw new Error(); + }); + + expect(await controller.validateToken({ jwt: 'token' })).toEqual(false); + }); +}); diff --git a/src/autenticacao/autenticacao.controller.ts b/src/autenticacao/autenticacao.controller.ts new file mode 100644 index 0000000..ca68eb4 --- /dev/null +++ b/src/autenticacao/autenticacao.controller.ts @@ -0,0 +1,29 @@ +import { Body, Controller, HttpCode, Post } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { HttpResponse } from '../shared/classes/http-response'; +import { PublicRoute } from '../shared/decorators/public-route.decorator'; +import { Response } from '../shared/interceptors/data-transform.interceptor'; +import { AutenticacaoService } from './autenticacao.service'; +import { LoginDto } from './dto/login.dto'; + +@Controller() +export class AutenticacaoController { + constructor(private readonly _service: AutenticacaoService) {} + + @Post('login') + @PublicRoute() + @HttpCode(200) + async login(@Body() body: LoginDto): Promise> { + const jwtToken = await this._service.login(body); + return new HttpResponse(jwtToken).onLogin(); + } + + @MessagePattern({ role: 'auth', cmd: 'check' }) + async validateToken(data: { jwt: string }) { + try { + return this._service.validateToken(data.jwt); + } catch (error) { + return false; + } + } +} diff --git a/src/autenticacao/autenticacao.module.ts b/src/autenticacao/autenticacao.module.ts new file mode 100644 index 0000000..461f1ad --- /dev/null +++ b/src/autenticacao/autenticacao.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { UsuarioModule } from '../usuario/usuario.module'; +import { AutenticacaoController } from './autenticacao.controller'; +import { AutenticacaoService } from './autenticacao.service'; +import { JwtStrategy } from './jwt.strategy'; + +@Module({ + imports: [ + UsuarioModule, + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_TOKEN_SECRET'), + }), + inject: [ConfigService], + }), + ], + controllers: [AutenticacaoController], + providers: [AutenticacaoService, JwtStrategy], + exports: [AutenticacaoService], +}) +export class AutenticacaoModule {} diff --git a/src/autenticacao/autenticacao.service.spec.ts b/src/autenticacao/autenticacao.service.spec.ts new file mode 100644 index 0000000..ef7e829 --- /dev/null +++ b/src/autenticacao/autenticacao.service.spec.ts @@ -0,0 +1,87 @@ +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { Test, TestingModule } from '@nestjs/testing'; +import bcrypt from 'bcrypt'; +import { UsuarioService } from '../usuario/usuario.service'; +import { AutenticacaoService } from './autenticacao.service'; + +describe('AutenticacaoService', () => { + let service: AutenticacaoService; + let usuarioService: UsuarioService; + let configService: ConfigService; + let jwtService: JwtService; + + const mockUsuarioService = { + findByEmail: jest.fn(), + }; + + const mockConfigService = { + get: jest.fn(), + }; + + const mockJwtService = { + sign: jest.fn(), + verify: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AutenticacaoService, + { provide: UsuarioService, useValue: mockUsuarioService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: JwtService, useValue: mockJwtService }, + ], + }).compile(); + + service = module.get(AutenticacaoService); + usuarioService = module.get(UsuarioService); + configService = module.get(ConfigService); + jwtService = module.get(JwtService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should not login - no email found', async () => { + jest.spyOn(usuarioService, 'findByEmail').mockReturnValue(undefined as any); + + expect(service.login({ email: 'a', senha: '1' })).rejects.toThrow( + new BadRequestException('Este email não está cadastrado!'), + ); + }); + + it('should not login - wrong password', async () => { + jest.spyOn(usuarioService, 'findByEmail').mockReturnValue('a' as any); + jest + .spyOn(bcrypt, 'compare') + .mockImplementation((pass: string | Buffer, hash: string) => + Promise.resolve(false), + ); + + expect(service.login({ email: 'a', senha: '1' })).rejects.toThrow( + new UnauthorizedException('Senha incorreta!'), + ); + }); + + it('should login', async () => { + jest.spyOn(configService, 'get').mockReturnValue('12h'); + jest.spyOn(jwtService, 'sign').mockReturnValue('token'); + jest.spyOn(usuarioService, 'findByEmail').mockReturnValue('a' as any); + jest + .spyOn(bcrypt, 'compare') + .mockImplementation((pass: string | Buffer, hash: string) => + Promise.resolve(true), + ); + + expect(await service.login({ email: 'a', senha: '1' })).toEqual('token'); + }); + + it('should validate token', () => { + jest.spyOn(jwtService, 'verify').mockReturnValue(true as any); + + expect(service.validateToken('token')).toEqual(true); + }); +}); diff --git a/src/autenticacao/autenticacao.service.ts b/src/autenticacao/autenticacao.service.ts new file mode 100644 index 0000000..d92f2f5 --- /dev/null +++ b/src/autenticacao/autenticacao.service.ts @@ -0,0 +1,53 @@ +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import bcrypt from 'bcrypt'; +import { UsuarioService } from '../usuario/usuario.service'; +import { LoginDto } from './dto/login.dto'; + +@Injectable() +export class AutenticacaoService { + constructor( + private readonly _usuarioService: UsuarioService, + private readonly _jwtService: JwtService, + private readonly _configService: ConfigService, + ) {} + + async login({ email, senha }: LoginDto): Promise { + const userFound = await this._usuarioService.findByEmail(email); + + if (!userFound) { + throw new BadRequestException('Este email não está cadastrado!'); + } + + await this.verifyPassword(senha, userFound.senha); + + const JwtPayload = { + id: userFound.id, + email: userFound.email, + nome: userFound.nome, + admin: userFound.admin, + }; + + const expiresIn = this._configService.get('JWT_TOKEN_EXPIRES_IN'); + + return this._jwtService.sign(JwtPayload, { expiresIn }); + } + + private async verifyPassword(senha: string, hash: string) { + // TODO a senha deve vir do front como um base64 + const isMatch = await bcrypt.compare(senha, hash); + + if (!isMatch) { + throw new UnauthorizedException('Senha incorreta!'); + } + } + + validateToken(jwt: string) { + return this._jwtService.verify(jwt); + } +} diff --git a/src/autenticacao/dto/login.dto.ts b/src/autenticacao/dto/login.dto.ts new file mode 100644 index 0000000..fe4d6eb --- /dev/null +++ b/src/autenticacao/dto/login.dto.ts @@ -0,0 +1,12 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class LoginDto { + @IsString() + @IsNotEmpty() + @IsEmail() + email!: string; + + @IsString() + @IsNotEmpty() + senha!: string; +} From 3b5a99aa97cd93bd11f1cd3314bcf0cab93bb476 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:11:32 -0300 Subject: [PATCH 8/9] (#136) aplica guard e autenticacao ao app module --- src/app.controller.ts | 2 ++ src/app.module.ts | 12 +++++++++++- src/usuario/usuario.controller.ts | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app.controller.ts b/src/app.controller.ts index cea3966..ded1087 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,11 +1,13 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; +import { PublicRoute } from './shared/decorators/public-route.decorator'; @Controller() export class AppController { constructor(private readonly service: AppService) {} @Get('health-check') + @PublicRoute() healthCheck() { return this.service.healthCheck(); } diff --git a/src/app.module.ts b/src/app.module.ts index 194408c..236a21d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,8 +1,11 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AutenticacaoModule } from './autenticacao/autenticacao.module'; +import { JwtAuthGuard } from './autenticacao/jwt-auth.guard'; import { DbModule } from './config/db/db.module'; import { DbService } from './config/db/db.service'; import { UsuarioModule } from './usuario/usuario.module'; @@ -21,8 +24,15 @@ const ENV = process.env.NODE_ENV; }), DbModule, UsuarioModule, + AutenticacaoModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], }) export class AppModule {} diff --git a/src/usuario/usuario.controller.ts b/src/usuario/usuario.controller.ts index 225e19d..b9f1b2d 100644 --- a/src/usuario/usuario.controller.ts +++ b/src/usuario/usuario.controller.ts @@ -11,6 +11,7 @@ import { HttpResponse } from '../shared/classes/http-response'; import { Filtering, Filtrate } from '../shared/decorators/filtrate.decorator'; import { Ordenate, Ordering } from '../shared/decorators/ordenate.decorator'; import { Paginate, Pagination } from '../shared/decorators/paginate.decorator'; +import { PublicRoute } from '../shared/decorators/public-route.decorator'; import { Response } from '../shared/interceptors/data-transform.interceptor'; import { ResponsePaginate } from '../shared/interfaces/response-paginate.interface'; import { IdValidator } from '../shared/validators/id.validator'; @@ -25,8 +26,10 @@ export class UsuarioController { constructor(private readonly _service: UsuarioService) {} @Post() + @PublicRoute() async create(@Body() body: CreateUsuarioDto): Promise> { const created = await this._service.create(body); + created.senha = ''; return new HttpResponse(created).onCreated(); } From 684e6a7adef7ff365486320c8dc88701731024b1 Mon Sep 17 00:00:00 2001 From: Henrique Melo Date: Sun, 22 Oct 2023 20:11:38 -0300 Subject: [PATCH 9/9] (#136) adiciona testes e2e --- e2e/usuario.e2e-spec.ts | 106 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/e2e/usuario.e2e-spec.ts b/e2e/usuario.e2e-spec.ts index dcf3699..5b0dc85 100644 --- a/e2e/usuario.e2e-spec.ts +++ b/e2e/usuario.e2e-spec.ts @@ -1,6 +1,8 @@ import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { ClientProxy, ClientsModule, Transport } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { lastValueFrom, timeout } from 'rxjs'; import request from 'supertest'; import { Repository } from 'typeorm'; import { AppModule } from '../src/app.module'; @@ -11,19 +13,35 @@ import { Usuario } from '../src/usuario/entities/usuario.entity'; describe('E2E - Usuario', () => { let app: INestApplication; + let client: ClientProxy; let repository: Repository; + let token: string; + + const senha = '123'; const user: Partial = { id: undefined, nome: 'Henrique', email: 'hacmelo@gmail.com', - senha: '123', + senha, admin: false, }; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], + imports: [ + AppModule, + ClientsModule.register([ + { + name: 'AUTH_CLIENT_TEST', + transport: Transport.TCP, + options: { + host: '0.0.0.0', + port: 8001, + }, + }, + ]), + ], }).compile(); app = moduleFixture.createNestApplication(); @@ -40,9 +58,20 @@ describe('E2E - Usuario', () => { new ModelNotFoundExceptionFilter(), ); + app.connectMicroservice({ + transport: Transport.TCP, + options: { + host: '0.0.0.0', + port: 8001, + }, + }); + await app.startAllMicroservices(); await app.init(); + client = app.get('AUTH_CLIENT_TEST'); + await client.connect(); + repository = app.get>(getRepositoryToken(Usuario)); }); @@ -101,11 +130,78 @@ describe('E2E - Usuario', () => { }); }); + describe('POST - /api/usuario/login', () => { + it('should not successfully login - wrong password', async () => { + const res = await request(app.getHttpServer()) + .post('/login') + .set('Content-Type', 'application/json') + .send({ email: user.email, senha: '1234' }); + + expect(res.statusCode).toEqual(401); + expect(res.body.message).toBe('Senha incorreta!'); + expect(res.body.data).toBeNull(); + }); + + it('should not successfully login - email not found', async () => { + const res = await request(app.getHttpServer()) + .post('/login') + .set('Content-Type', 'application/json') + .send({ email: 'a@a.com', senha }); + + expect(res.statusCode).toEqual(400); + expect(res.body.message).toBe('Este email não está cadastrado!'); + expect(res.body.data).toBeNull(); + }); + + it('should not get when not authenticated', async () => { + const res = await request(app.getHttpServer()) + .get('') + .set('Content-Type', 'application/json') + .send(); + + expect(res.statusCode).toEqual(401); + expect(res.body.message).toBe('Usuário não autenticado!'); + expect(res.body.data).toBeNull(); + }); + + it('should successfully login', async () => { + const res = await request(app.getHttpServer()) + .post('/login') + .set('Content-Type', 'application/json') + .send({ email: user.email, senha }); + + expect(res.statusCode).toEqual(200); + expect(res.body.message).toBe('Login efetuado com sucesso!'); + expect(res.body.data).toBeDefined(); + + token = res.body.data; + }); + + it('should validate token', async () => { + const request = client + .send({ role: 'auth', cmd: 'check' }, { jwt: token }) + .pipe(timeout(5000)); + + const response = await lastValueFrom(request); + expect(response.email).toBe(user.email); + }); + + it('should not validate token', async () => { + const request = client + .send({ role: 'auth', cmd: 'check' }, { jwt: 'invalid token' }) + .pipe(timeout(5000)); + + const response = await lastValueFrom(request); + expect(response).toBe(false); + }); + }); + describe('GET - /api/usuario/:id', () => { it('should successfully get "usuario" by id', async () => { const res = await request(app.getHttpServer()) .get(`/${user.id}`) .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) .send(); expect(res.statusCode).toEqual(200); @@ -118,6 +214,7 @@ describe('E2E - Usuario', () => { const res = await request(app.getHttpServer()) .get(`/${wrongId}`) .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) .send(); expect(res.statusCode).toEqual(400); @@ -130,6 +227,7 @@ describe('E2E - Usuario', () => { const res = await request(app.getHttpServer()) .get('/9999') .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) .send(); expect(res.statusCode).toEqual(404); @@ -149,6 +247,7 @@ describe('E2E - Usuario', () => { const res = await request(app.getHttpServer()) .get('?filter=' + JSON.stringify(filter)) .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) .send(); expect(res.statusCode).toEqual(200); @@ -164,6 +263,7 @@ describe('E2E - Usuario', () => { const res = await request(app.getHttpServer()) .patch(`/${user.id}`) .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) .send(update); user.nome = update.nome; @@ -179,6 +279,7 @@ describe('E2E - Usuario', () => { const res = await request(app.getHttpServer()) .delete(`/${user.id}`) .set('Content-Type', 'application/json') + .set('Authorization', 'bearer ' + token) .send(); delete user.id; @@ -193,5 +294,6 @@ describe('E2E - Usuario', () => { await repository.query('TRUNCATE usuario CASCADE'); await repository.delete({}); await app.close(); + await client.close(); }); });