├── pictures ├── 27.png ├── 39.jpg └── 39.png ├── dev └── images │ ├── cat.png │ ├── man.png │ └── nest.jpg ├── src ├── common │ ├── regex │ │ ├── regex.protocol.ts │ │ ├── remove-spaces.regex.ts │ │ ├── only-lowercase-letters.regex.ts │ │ └── regex.factory.ts │ ├── params │ │ ├── url-param.decorator.ts │ │ └── req-data-param.decorator.ts │ ├── dto │ │ └── pagination.dto.ts │ ├── guards │ │ └── is-admin.guard.ts │ ├── interceptors │ │ ├── add-header.interceptor.ts │ │ ├── change-data.interceptor.ts │ │ ├── auth-token.interceptor.ts │ │ ├── timing-connection.interceptor.ts │ │ ├── error-handling.interceptor.ts │ │ └── simple-cache.interceptor.ts │ ├── filters │ │ ├── error-exception.filter.ts │ │ └── my-exception.filter.ts │ ├── middlewares │ │ ├── outro.middleware.ts │ │ └── simple.middleware.ts │ └── pipes │ │ └── parse-int-id.pipe.ts ├── auth │ ├── auth.constants.ts │ ├── dto │ │ ├── refresh-token.dto.ts │ │ ├── token-payload.dto.ts │ │ └── login.dto.ts │ ├── hashing │ │ ├── hashing.service.ts │ │ └── bcrypt.service.ts │ ├── decorators │ │ └── set-route-policy.decorator.ts │ ├── config │ │ └── jwt.config.ts │ ├── enum │ │ └── route-policies.enum.ts │ ├── params │ │ └── token-payload.param.ts │ ├── auth.controller.ts │ ├── guards │ │ ├── auth-and-policy.guard.ts │ │ ├── route-policy.guard.ts │ │ └── auth-token.guard.ts │ ├── auth.module.ts │ └── auth.service.ts ├── recados │ ├── recados.config.ts │ ├── recados.constant.ts │ ├── recados.utils.ts │ ├── dto │ │ ├── update-recado.dto.ts │ │ ├── create-recado.dto.ts │ │ └── response-recado.dto.ts │ ├── recados.module.ts │ ├── entities │ │ └── recado.entity.ts │ ├── recados.service.ts │ └── recados.controller.ts ├── pessoas │ ├── dto │ │ ├── update-pessoa.dto.ts │ │ ├── create-pessoa.dto.ts │ │ └── create-pessoa.dto.spec.ts │ ├── pessoas.module.ts │ ├── entities │ │ └── pessoa.entity.ts │ ├── pessoas.controller.ts │ ├── pessoas.controller.spec.ts │ ├── pessoas.service.ts │ └── pessoas.service.spec.ts ├── email │ ├── email.module.ts │ └── email.service.ts ├── global-config │ ├── global-config.module.ts │ └── global.config.ts ├── conceitos-manual │ ├── conceitos-manual.service.ts │ ├── conceitos-manual.module.ts │ └── conceitos-manual.controller.ts ├── conceitos-automatico │ ├── conceitos-automatico.service.ts │ ├── conceitos-automatico.module.ts │ └── conceitos-automatico.controller.ts ├── app │ ├── app.service.ts │ ├── config │ │ └── app.config.ts │ ├── app.controller.ts │ └── app.module.ts ├── my-dynamic │ └── my-dynamic.module.ts └── main.ts ├── tsconfig.build.json ├── nest-cli.json ├── test ├── jest-e2e.json └── pessoas.e2e-spec.ts ├── .prettierrc ├── tsconfig.json ├── .env-example ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── client.rest ├── package.json ├── README.md ├── SERVER.md └── AULAS.md /pictures/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizomf/conceitos_nest/HEAD/pictures/27.png -------------------------------------------------------------------------------- /pictures/39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizomf/conceitos_nest/HEAD/pictures/39.jpg -------------------------------------------------------------------------------- /pictures/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizomf/conceitos_nest/HEAD/pictures/39.png -------------------------------------------------------------------------------- /dev/images/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizomf/conceitos_nest/HEAD/dev/images/cat.png -------------------------------------------------------------------------------- /dev/images/man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizomf/conceitos_nest/HEAD/dev/images/man.png -------------------------------------------------------------------------------- /dev/images/nest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizomf/conceitos_nest/HEAD/dev/images/nest.jpg -------------------------------------------------------------------------------- /src/common/regex/regex.protocol.ts: -------------------------------------------------------------------------------- 1 | export interface RegexProtocol { 2 | execute(str: string): string; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/auth.constants.ts: -------------------------------------------------------------------------------- 1 | export const REQUEST_TOKEN_PAYLOAD_KEY = 'REQUEST_TOKEN_PAYLOAD_KEY'; 2 | export const ROUTE_POLICY_KEY = 'ROUTE_POLICY_KEY'; 3 | -------------------------------------------------------------------------------- /src/auth/dto/refresh-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class RefreshTokenDto { 4 | @IsNotEmpty() 5 | refreshToken: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/auth/dto/token-payload.dto.ts: -------------------------------------------------------------------------------- 1 | export class TokenPayloadDto { 2 | sub: number; 3 | email: string; 4 | iat: number; 5 | exp: number; 6 | aud: string; 7 | iss: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/recados/recados.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('recados', () => ({ 4 | teste1: 'VALOR 1', 5 | teste2: 'VALOR 2', 6 | })); 7 | -------------------------------------------------------------------------------- /src/auth/hashing/hashing.service.ts: -------------------------------------------------------------------------------- 1 | export abstract class HashingService { 2 | abstract hash(password: string): Promise; 3 | abstract compare(password: string, passwordHash: string): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/recados/recados.constant.ts: -------------------------------------------------------------------------------- 1 | export const SERVER_NAME = 'SERVER_NAME'; 2 | export const ONLY_LOWERCASE_LETTERS_REGEX = 'ONLY_LOWERCASE_LETTERS_REGEX'; 3 | export const REMOVE_SPACES_REGEX = 'REMOVE_SPACES_REGEX'; 4 | -------------------------------------------------------------------------------- /src/pessoas/dto/update-pessoa.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreatePessoaDto } from './create-pessoa.dto'; 3 | 4 | export class UpdatePessoaDto extends PartialType(CreatePessoaDto) {} 5 | -------------------------------------------------------------------------------- /src/email/email.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailService } from './email.service'; 3 | 4 | @Module({ 5 | providers: [EmailService], 6 | exports: [EmailService], 7 | }) 8 | export class EmailModule {} 9 | -------------------------------------------------------------------------------- /src/global-config/global-config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | 4 | @Module({ 5 | imports: [ConfigModule.forRoot({})], 6 | }) 7 | export class GlobalConfigModule {} 8 | -------------------------------------------------------------------------------- /src/auth/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class LoginDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | @IsNotEmpty() 9 | password: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/regex/remove-spaces.regex.ts: -------------------------------------------------------------------------------- 1 | import { RegexProtocol } from './regex.protocol'; 2 | 3 | export class RemoveSpacesRegex implements RegexProtocol { 4 | execute(str: string): string { 5 | return str.replace(/\s+/g, ''); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/conceitos-manual/conceitos-manual.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class ConceitosManualService { 5 | solucionaHome(): string { 6 | return 'Home do Conceitos Manual Solucionada.'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "plugins": ["@nestjs/swagger/plugin"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/regex/only-lowercase-letters.regex.ts: -------------------------------------------------------------------------------- 1 | import { RegexProtocol } from './regex.protocol'; 2 | 3 | export class OnlyLowercaseLettersRegex implements RegexProtocol { 4 | execute(str: string): string { 5 | return str.replace(/[^a-z]/g, ''); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/conceitos-automatico/conceitos-automatico.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class ConceitosAutomaticoService { 5 | getHome() { 6 | return 'conceitos-automatico (ConceitosAutomaticoService)'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | 9 | solucionaExemplo() { 10 | return 'Exemplo usa o service.'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "moduleNameMapper": { 10 | "^src/(.*)$": "/../src/$1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/auth/decorators/set-route-policy.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { ROUTE_POLICY_KEY } from '../auth.constants'; 3 | import { RoutePolicies } from '../enum/route-policies.enum'; 4 | 5 | export const SetRoutePolicy = (policy: RoutePolicies) => { 6 | return SetMetadata(ROUTE_POLICY_KEY, policy); 7 | }; 8 | -------------------------------------------------------------------------------- /src/conceitos-manual/conceitos-manual.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConceitosManualController } from './conceitos-manual.controller'; 3 | import { ConceitosManualService } from './conceitos-manual.service'; 4 | 5 | @Module({ 6 | controllers: [ConceitosManualController], 7 | providers: [ConceitosManualService], 8 | }) 9 | export class ConceitosManualModule {} 10 | -------------------------------------------------------------------------------- /src/common/params/url-param.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | 4 | export const UrlParam = createParamDecorator( 5 | (data: unknown, ctx: ExecutionContext) => { 6 | const context = ctx.switchToHttp(); 7 | const request: Request = context.getRequest(); 8 | return request.url; 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /src/common/dto/pagination.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { IsInt, IsOptional, Max, Min } from 'class-validator'; 3 | 4 | export class PaginationDto { 5 | @IsOptional() 6 | @IsInt() 7 | @Min(1) 8 | @Max(50) 9 | @Type(() => Number) 10 | limit: number; 11 | 12 | @IsOptional() 13 | @IsInt() 14 | @Min(0) 15 | @Type(() => Number) 16 | offset: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/params/req-data-param.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | 4 | export const ReqDataParam = createParamDecorator( 5 | (data: keyof Request, ctx: ExecutionContext) => { 6 | const context = ctx.switchToHttp(); 7 | const request: Request = context.getRequest(); 8 | return request[data]; 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /src/auth/config/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('jwt', () => { 4 | return { 5 | secret: process.env.JWT_SECRET, 6 | audience: process.env.JWT_TOKEN_AUDIENCE, 7 | issuer: process.env.JWT_TOKEN_ISSUER, 8 | jwtTtl: Number(process.env.JWT_TTL ?? '3600'), 9 | jwtRefreshTtl: Number(process.env.JWT_REFRESH_TTL ?? '86400'), 10 | }; 11 | }); 12 | -------------------------------------------------------------------------------- /src/conceitos-automatico/conceitos-automatico.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConceitosAutomaticoController } from './conceitos-automatico.controller'; 3 | import { ConceitosAutomaticoService } from './conceitos-automatico.service'; 4 | 5 | @Module({ 6 | controllers: [ConceitosAutomaticoController], 7 | providers: [ConceitosAutomaticoService], 8 | }) 9 | export class ConceitosAutomaticoModule {} 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": true, 8 | "printWidth": 80, 9 | "proseWrap": "always", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "all", 16 | "useTabs": false 17 | } 18 | -------------------------------------------------------------------------------- /src/auth/enum/route-policies.enum.ts: -------------------------------------------------------------------------------- 1 | export enum RoutePolicies { 2 | createRecado = 'createRecado', 3 | findOneRecado = 'findOneRecado', 4 | findAllRecados = 'findAllRecados', 5 | updateRecado = 'updateRecado', 6 | deleteRecado = 'deleteRecado', 7 | 8 | createPessoa = 'createPessoa', 9 | findOnePessoa = 'findOnePessoa', 10 | findAllPessoas = 'findAllPessoas', 11 | updatePessoa = 'updatePessoa', 12 | deletePessoa = 'deletePessoa', 13 | } 14 | -------------------------------------------------------------------------------- /src/recados/recados.utils.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class RecadosUtils { 5 | inverteString(str: string) { 6 | // Luiz -> ziuL 7 | console.log('NÃO É MOCK'); 8 | return str.split('').reverse().join(''); 9 | } 10 | } 11 | 12 | @Injectable() 13 | export class RecadosUtilsMock { 14 | inverteString() { 15 | console.log('Passei no MOCK'); 16 | return 'bla bla bla'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/conceitos-manual/conceitos-manual.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ConceitosManualService } from './conceitos-manual.service'; 3 | 4 | @Controller('conceitos-manual') 5 | export class ConceitosManualController { 6 | constructor( 7 | private readonly conceitosManualService: ConceitosManualService, 8 | ) {} 9 | 10 | @Get() 11 | home(): string { 12 | return this.conceitosManualService.solucionaHome(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/auth/params/token-payload.param.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | import { REQUEST_TOKEN_PAYLOAD_KEY } from '../auth.constants'; 4 | 5 | export const TokenPayloadParam = createParamDecorator( 6 | (data: unknown, ctx: ExecutionContext) => { 7 | const context = ctx.switchToHttp(); 8 | const request: Request = context.getRequest(); 9 | return request[REQUEST_TOKEN_PAYLOAD_KEY]; 10 | }, 11 | ); 12 | -------------------------------------------------------------------------------- /src/common/guards/is-admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class IsAdminGuard implements CanActivate { 6 | canActivate( 7 | context: ExecutionContext, 8 | ): boolean | Promise | Observable { 9 | const request = context.switchToHttp().getRequest(); 10 | const role = request['user']?.role; 11 | return role === 'admin'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/conceitos-automatico/conceitos-automatico.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ConceitosAutomaticoService } from './conceitos-automatico.service'; 3 | 4 | @Controller('conceitos-automatico') 5 | export class ConceitosAutomaticoController { 6 | constructor( 7 | private readonly conceitosAutomaticoService: ConceitosAutomaticoService, 8 | ) {} 9 | 10 | @Get() 11 | home(): string { 12 | return this.conceitosAutomaticoService.getHome(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/auth/hashing/bcrypt.service.ts: -------------------------------------------------------------------------------- 1 | import { HashingService } from './hashing.service'; 2 | import * as bcrypt from 'bcryptjs'; 3 | 4 | export class BcryptService extends HashingService { 5 | async hash(password: string): Promise { 6 | const salt = await bcrypt.genSalt(); 7 | return bcrypt.hash(password, salt); // gera um hash 8 | } 9 | 10 | async compare(password: string, passwordHash: string): Promise { 11 | return bcrypt.compare(password, passwordHash); // true === logado 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pessoas/dto/create-pessoa.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsEmail, 3 | IsNotEmpty, 4 | IsString, 5 | MaxLength, 6 | MinLength, 7 | } from 'class-validator'; 8 | 9 | export class CreatePessoaDto { 10 | @IsEmail() 11 | email: string; 12 | 13 | @IsString() 14 | @IsNotEmpty() 15 | @MinLength(5) 16 | password: string; 17 | 18 | @IsString() 19 | @IsNotEmpty() 20 | @MinLength(3) 21 | @MaxLength(100) 22 | nome: string; 23 | 24 | // @IsEnum(RoutePolicies, { each: true }) 25 | // routePolicies: RoutePolicies[]; 26 | } 27 | -------------------------------------------------------------------------------- /src/pessoas/pessoas.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PessoasService } from './pessoas.service'; 3 | import { PessoasController } from './pessoas.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Pessoa } from './entities/pessoa.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Pessoa])], 9 | controllers: [PessoasController], 10 | providers: [PessoasService], 11 | exports: [PessoasService], // ao importar este módulo em outro módulo 12 | }) 13 | export class PessoasModule {} 14 | -------------------------------------------------------------------------------- /src/recados/dto/update-recado.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, PartialType } from '@nestjs/swagger'; 2 | import { CreateRecadoDto } from './create-recado.dto'; 3 | import { IsBoolean, IsOptional } from 'class-validator'; 4 | 5 | export class UpdateRecadoDto extends PartialType(CreateRecadoDto) { 6 | @ApiProperty({ 7 | example: true, 8 | description: 'Indica se o recado foi lido ou não', 9 | required: false, 10 | }) 11 | @IsBoolean() 12 | @IsOptional() 13 | readonly lido?: boolean; 14 | 15 | // @ApiHideProperty() 16 | // readonly testando?: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/config/app.config.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, ValidationPipe } from '@nestjs/common'; 2 | import { ParseIntIdPipe } from 'src/common/pipes/parse-int-id.pipe'; 3 | 4 | export default (app: INestApplication) => { 5 | app.useGlobalPipes( 6 | new ValidationPipe({ 7 | whitelist: true, // remove chaves que não estão no DTO 8 | forbidNonWhitelisted: true, // levantar erro quando a chave não existir 9 | transform: false, // tenta transformar os tipos de dados de param e dtos 10 | }), 11 | new ParseIntIdPipe(), 12 | ); 13 | return app; 14 | }; 15 | -------------------------------------------------------------------------------- /src/common/interceptors/add-header.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class AddHeaderInterceptor implements NestInterceptor { 10 | async intercept(context: ExecutionContext, next: CallHandler) { 11 | console.log('AddHeaderInterceptor executado.'); 12 | 13 | const response = context.switchToHttp().getResponse(); 14 | 15 | response.setHeader('X-Custom-Header', 'O valor do cabeçalho'); 16 | 17 | return next.handle(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/global-config/global.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('globalConfig', () => ({ 4 | database: { 5 | type: process.env.DATABASE_TYPE as 'postgres', 6 | host: process.env.DATABASE_HOST, 7 | port: +process.env.DATABASE_PORT, 8 | username: process.env.DATABASE_USERNAME, 9 | database: process.env.DATABASE_DATABASE, 10 | password: process.env.DATABASE_PASSWORD, 11 | autoLoadEntities: Boolean(process.env.DATABASE_AUTOLOADENTITIES), 12 | synchronize: Boolean(process.env.DATABASE_SYNCHRONIZE), 13 | }, 14 | environment: process.env.NODE_ENV || 'development', 15 | })); 16 | -------------------------------------------------------------------------------- /src/common/interceptors/change-data.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { map } from 'rxjs'; 8 | 9 | @Injectable() 10 | export class ChangeDataInterceptor implements NestInterceptor { 11 | async intercept(context: ExecutionContext, next: CallHandler) { 12 | return next.handle().pipe( 13 | map(data => { 14 | if (Array.isArray(data)) { 15 | return { 16 | data, 17 | count: data.length, 18 | }; 19 | } 20 | 21 | return data; 22 | }), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { LoginDto } from './dto/login.dto'; 4 | import { RefreshTokenDto } from './dto/refresh-token.dto'; 5 | 6 | @Controller('auth') 7 | export class AuthController { 8 | constructor(private readonly authService: AuthService) {} 9 | 10 | @Post() 11 | login(@Body() loginDto: LoginDto) { 12 | return this.authService.login(loginDto); 13 | } 14 | 15 | @Post('refresh') 16 | refreshTokens(@Body() refreshTokenDto: RefreshTokenDto) { 17 | return this.authService.refreshTokens(refreshTokenDto); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/interceptors/auth-token.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class AuthTokenInterceptor implements NestInterceptor { 11 | async intercept(context: ExecutionContext, next: CallHandler) { 12 | const request = context.switchToHttp().getRequest(); 13 | const token = request.headers.authorization?.split(' ')[1]; 14 | 15 | // CHECAR O TOKEN 16 | if (!token || token != '123456') { 17 | throw new UnauthorizedException('Usuário não logado'); 18 | } 19 | 20 | return next.handle(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | DATABASE_TYPE='postgres' 2 | DATABASE_HOST='localhost' 3 | DATABASE_PORT=5432 4 | DATABASE_USERNAME='DATABASE_USERNAME GOES HERE' 5 | DATABASE_DATABASE='DATABASE_DATABASE GOES HERE' 6 | DATABASE_PASSWORD='DATABASE_PASSWORD GOES HERE' 7 | DATABASE_AUTOLOADENTITIES=1 8 | DATABASE_SYNCHRONIZE=1 9 | 10 | JWT_SECRET='JWT_SECRET GOES HERE' 11 | JWT_TOKEN_AUDIENCE=http://localhost:3000 12 | JWT_TOKEN_ISSUER=http://localhost:3000 13 | JWT_TTL=3600 14 | JWT_REFRESH_TTL=86400 15 | 16 | APP_PORT=3000 17 | NODE_ENV='development' 18 | 19 | EMAIL_HOST=smtp.example.com 20 | EMAIL_PORT=587 21 | EMAIL_USER='EMAIL_USER GOES HERE' 22 | EMAIL_PASS='EMAIL_USER GOES HERE' 23 | EMAIL_FROM=no-reply@example.com 24 | -------------------------------------------------------------------------------- /src/recados/dto/create-recado.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsNotEmpty, 4 | IsPositive, 5 | IsString, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | export class CreateRecadoDto { 11 | @ApiProperty({ 12 | example: 'Este é um recado de exemplo', 13 | description: 'O conteúdo textual do recado', 14 | minLength: 5, 15 | maxLength: 255, 16 | }) 17 | @IsString() 18 | @IsNotEmpty() 19 | @MinLength(5) 20 | @MaxLength(255) 21 | readonly texto: string; 22 | 23 | @ApiProperty({ 24 | example: 2, 25 | description: 'ID do destinatário do recado', 26 | }) 27 | @IsPositive() 28 | paraId: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Inject } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { ConfigType } from '@nestjs/config'; 4 | import globalConfig from 'src/global-config/global.config'; 5 | 6 | @Controller('home') // /home 7 | export class AppController { 8 | constructor( 9 | private readonly appService: AppService, 10 | @Inject(globalConfig.KEY) 11 | private readonly globalConfiguration: ConfigType, 12 | ) {} 13 | 14 | // @Get('hello') 15 | getHello(): string { 16 | const retorno = 'Retorno.'; 17 | return retorno; 18 | } 19 | 20 | // @Get('exemplo') 21 | exemplo() { 22 | return this.appService.solucionaExemplo(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auth/guards/auth-and-policy.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { AuthTokenGuard } from './auth-token.guard'; 3 | import { RoutePolicyGuard } from './route-policy.guard'; 4 | 5 | @Injectable() 6 | export class AuthAndPolicyGuard implements CanActivate { 7 | constructor( 8 | private readonly authTokenGuard: AuthTokenGuard, 9 | private readonly routePolicyGuard: RoutePolicyGuard, 10 | ) {} 11 | 12 | async canActivate(context: ExecutionContext): Promise { 13 | const isAuthValid = await this.authTokenGuard.canActivate(context); 14 | 15 | if (!isAuthValid) return false; 16 | 17 | return this.routePolicyGuard.canActivate(context); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/common/filters/error-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | 3 | @Catch(Error) 4 | export class ErrorExceptionFilter implements ExceptionFilter { 5 | catch(exception: any, host: ArgumentsHost) { 6 | const context = host.switchToHttp(); 7 | 8 | const response = context.getResponse(); 9 | const request = context.getRequest(); 10 | 11 | const statusCode = exception.getStatus ? exception.getStatus() : 400; 12 | const exceptionResponse = exception.getResponse 13 | ? exception.getResponse() 14 | : { message: 'Error', statusCode }; 15 | 16 | response.status(statusCode).json({ 17 | ...exceptionResponse, 18 | data: new Date().toISOString(), 19 | path: request.url, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/middlewares/outro.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NestMiddleware } from '@nestjs/common'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | export class OutroMiddleware implements NestMiddleware { 5 | use(req: Request, res: Response, next: NextFunction) { 6 | console.log('OutroMiddleware: Olá'); 7 | const authorization = req.headers?.authorization; 8 | 9 | if (authorization) { 10 | req['user'] = { 11 | nome: 'Luiz', 12 | sobrenome: 'Otávio', 13 | }; 14 | } 15 | 16 | res.setHeader('CABECALHO', 'Do Middleware'); 17 | 18 | // Terminando a cadeia de chamadas 19 | // return res.status(404).send({ 20 | // message: 'Não encontrado', 21 | // }); 22 | 23 | next(); 24 | 25 | console.log('OutroMiddleware: Tchau'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/common/pipes/parse-int-id.pipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentMetadata, 3 | BadRequestException, 4 | Injectable, 5 | PipeTransform, 6 | } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class ParseIntIdPipe implements PipeTransform { 10 | transform(value: any, metadata: ArgumentMetadata) { 11 | if (metadata.type !== 'param' || metadata.data !== 'id') { 12 | return value; 13 | } 14 | 15 | const parsedValue = Number(value); 16 | 17 | if (isNaN(parsedValue)) { 18 | throw new BadRequestException( 19 | 'ParseIntIdPipe espera uma string numérica.', 20 | ); 21 | } 22 | 23 | if (parsedValue < 0) { 24 | throw new BadRequestException( 25 | 'ParseIntIdPipe espera um número maior do que zero.', 26 | ); 27 | } 28 | 29 | return parsedValue; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/common/regex/regex.factory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, InternalServerErrorException } from '@nestjs/common'; 2 | import { OnlyLowercaseLettersRegex } from './only-lowercase-letters.regex'; 3 | import { RemoveSpacesRegex } from './remove-spaces.regex'; 4 | import { RegexProtocol } from './regex.protocol'; 5 | 6 | export type ClassNames = 'OnlyLowercaseLettersRegex' | 'RemoveSpacesRegex'; 7 | 8 | @Injectable() 9 | export class RegexFactory { 10 | create(className: ClassNames): RegexProtocol { 11 | // Meu código/lógica 12 | switch (className) { 13 | case 'OnlyLowercaseLettersRegex': 14 | return new OnlyLowercaseLettersRegex(); 15 | case 'RemoveSpacesRegex': 16 | return new RemoveSpacesRegex(); 17 | default: 18 | throw new InternalServerErrorException( 19 | `No class found for ${className}`, 20 | ); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/common/interceptors/timing-connection.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { tap } from 'rxjs'; 8 | 9 | @Injectable() 10 | export class TimingConnectionInterceptor implements NestInterceptor { 11 | async intercept(context: ExecutionContext, next: CallHandler) { 12 | const startTime = Date.now(); 13 | 14 | console.log('TimingConnectionInterceptor executado ANTES'); 15 | 16 | // await new Promise(resolve => setTimeout(resolve, 10000)); 17 | 18 | return next.handle().pipe( 19 | tap(() => { 20 | const finalTime = Date.now(); 21 | const elapsedTime = finalTime - startTime; 22 | 23 | console.log( 24 | `TimingConnectionInterceptor: levou ${elapsedTime}ms para executar.`, 25 | ); 26 | }), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/recados/recados.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { RecadosController } from './recados.controller'; 3 | import { RecadosService } from './recados.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Recado } from './entities/recado.entity'; 6 | import { PessoasModule } from 'src/pessoas/pessoas.module'; 7 | import { RecadosUtils } from './recados.utils'; 8 | import { ConfigModule } from '@nestjs/config'; 9 | import recadosConfig from './recados.config'; 10 | import { EmailModule } from 'src/email/email.module'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forFeature(recadosConfig), 15 | TypeOrmModule.forFeature([Recado]), 16 | forwardRef(() => PessoasModule), 17 | EmailModule, 18 | ], 19 | controllers: [RecadosController], 20 | providers: [RecadosService, RecadosUtils], 21 | exports: [RecadosUtils], 22 | }) 23 | export class RecadosModule {} 24 | -------------------------------------------------------------------------------- /src/common/filters/my-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | } from '@nestjs/common'; 7 | 8 | @Catch(HttpException) 9 | export class MyExceptionFilter 10 | implements ExceptionFilter 11 | { 12 | catch(exception: T, host: ArgumentsHost) { 13 | const context = host.switchToHttp(); 14 | const response = context.getResponse(); 15 | const request = context.getRequest(); 16 | 17 | const statusCode = exception.getStatus(); 18 | const exceptionResponse = exception.getResponse(); 19 | 20 | const error = 21 | typeof response === 'string' 22 | ? { 23 | message: exceptionResponse, 24 | } 25 | : (exceptionResponse as object); 26 | 27 | response.status(statusCode).json({ 28 | ...error, 29 | data: new Date().toISOString(), 30 | path: request.url, 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/interceptors/error-handling.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | CallHandler, 4 | ExecutionContext, 5 | Injectable, 6 | NestInterceptor, 7 | } from '@nestjs/common'; 8 | import { catchError, throwError } from 'rxjs'; 9 | 10 | @Injectable() 11 | export class ErrorHandlingInterceptor implements NestInterceptor { 12 | async intercept(context: ExecutionContext, next: CallHandler) { 13 | console.log('ErrorHandlingInterceptor executado ANTES'); 14 | 15 | // await new Promise(resolve => setTimeout(resolve, 10000)); 16 | 17 | return next.handle().pipe( 18 | catchError(error => { 19 | return throwError(() => { 20 | if (error.name === 'NotFoundException') { 21 | return new BadRequestException(error.message); 22 | } 23 | 24 | return new BadRequestException('Ocorreu um erro desconhecido.'); 25 | }); 26 | }), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | *.env 40 | .env 41 | .env.development.local 42 | .env.test.local 43 | .env.production.local 44 | .env.local 45 | 46 | # temp directory 47 | .temp 48 | .tmp 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Diagnostic reports (https://nodejs.org/api/report.html) 57 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 58 | 59 | package-lock.json -------------------------------------------------------------------------------- /src/common/interceptors/simple-cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { of, tap } from 'rxjs'; 8 | 9 | @Injectable() 10 | export class SimpleCacheInterceptor implements NestInterceptor { 11 | private readonly cache = new Map(); 12 | 13 | async intercept(context: ExecutionContext, next: CallHandler) { 14 | console.log('SimpleCacheInterceptor executado ANTES'); 15 | const request = context.switchToHttp().getRequest(); 16 | const url = request.url; 17 | 18 | if (this.cache.has(url)) { 19 | console.log('Está no cache', url); 20 | return of(this.cache.get(url)); 21 | } 22 | 23 | await new Promise(resolve => setTimeout(resolve, 3000)); 24 | 25 | return next.handle().pipe( 26 | tap(data => { 27 | this.cache.set(url, data); 28 | console.log('Armazenado em cache', url); 29 | }), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { HashingService } from './hashing/hashing.service'; 3 | import { BcryptService } from './hashing/bcrypt.service'; 4 | import { AuthController } from './auth.controller'; 5 | import { AuthService } from './auth.service'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { Pessoa } from 'src/pessoas/entities/pessoa.entity'; 8 | import { ConfigModule } from '@nestjs/config'; 9 | import jwtConfig from './config/jwt.config'; 10 | import { JwtModule } from '@nestjs/jwt'; 11 | 12 | @Global() 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forFeature([Pessoa]), 16 | ConfigModule.forFeature(jwtConfig), 17 | JwtModule.registerAsync(jwtConfig.asProvider()), 18 | ], 19 | controllers: [AuthController], 20 | providers: [ 21 | { 22 | provide: HashingService, 23 | useClass: BcryptService, 24 | }, 25 | AuthService, 26 | ], 27 | exports: [HashingService, JwtModule, ConfigModule, TypeOrmModule], 28 | }) 29 | export class AuthModule {} 30 | -------------------------------------------------------------------------------- /src/common/middlewares/simple.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NestMiddleware } from '@nestjs/common'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | export class SimpleMiddleware implements NestMiddleware { 5 | use(req: Request, res: Response, next: NextFunction) { 6 | console.log('SimpleMiddleware: Olá'); 7 | const authorization = req.headers?.authorization; 8 | 9 | if (authorization) { 10 | req['user'] = { 11 | nome: 'Luiz', 12 | sobrenome: 'Otávio', 13 | role: 'admin', 14 | }; 15 | } 16 | 17 | // if (authorization) { 18 | // throw new BadRequestException('Bla bla'); 19 | // } 20 | 21 | res.setHeader('CABECALHO', 'Do Middleware'); 22 | 23 | // Terminando a cadeia de chamadas 24 | // return res.status(404).send({ 25 | // message: 'Não encontrado', 26 | // }); 27 | 28 | next(); // Próximo middleware 29 | 30 | console.log('SimpleMiddleware: Tchau'); 31 | 32 | res.on('finish', () => { 33 | console.log('SimpleMiddleware: Terminou'); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/my-dynamic/my-dynamic.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | 3 | export type MyDynamicModuleConfigs = { 4 | apiKey: string; 5 | apiUrl: string; 6 | }; 7 | 8 | export const MY_DYNAMIC_CONFIG = 'MY_DYNAMIC_CONFIG'; 9 | 10 | @Module({}) 11 | export class MyDynamicModule { 12 | static register(myModuleConfigs: MyDynamicModuleConfigs): DynamicModule { 13 | // Aqui eu vou usar minhas configurações 14 | console.log('MyDynamicModule', myModuleConfigs); 15 | 16 | return { 17 | module: MyDynamicModule, 18 | imports: [], 19 | providers: [ 20 | { 21 | provide: MY_DYNAMIC_CONFIG, 22 | useFactory: async () => { 23 | console.log('MyDynamicModule: Aqui posso ter lógica'); 24 | await new Promise(res => setTimeout(res, 3000)); 25 | console.log('MyDynamicModule: TERMINOU A LÓGICA'); 26 | 27 | return myModuleConfigs; 28 | }, 29 | }, 30 | ], 31 | controllers: [], 32 | exports: [MY_DYNAMIC_CONFIG], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app/app.module'; 3 | import appConfig from './app/config/app.config'; 4 | import helmet from 'helmet'; 5 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | 10 | appConfig(app); 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | // helmet -> cabeçalhos de segurança no protocolo HTTP 14 | app.use(helmet()); 15 | 16 | // cors -> permitir que outro domínio faça requests na sua aplicação 17 | app.enableCors({ 18 | origin: 'https://meuapp.com.br', 19 | }); 20 | } 21 | 22 | const documentBuilderConfig = new DocumentBuilder() 23 | .setTitle('Recados API') 24 | .setDescription('Envie recados para seus amigos e familiares') 25 | .setVersion('1.0') 26 | .addBearerAuth() 27 | .build(); 28 | 29 | const document = SwaggerModule.createDocument(app, documentBuilderConfig); 30 | 31 | SwaggerModule.setup('docs', app, document); 32 | 33 | await app.listen(process.env.APP_PORT); 34 | } 35 | bootstrap(); 36 | -------------------------------------------------------------------------------- /src/email/email.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as nodemailer from 'nodemailer'; 3 | 4 | @Injectable() 5 | export class EmailService { 6 | private transporter: nodemailer.Transporter; 7 | 8 | constructor() { 9 | this.transporter = nodemailer.createTransport({ 10 | host: process.env.EMAIL_HOST, // Servidor SMTP 11 | port: parseInt(process.env.EMAIL_PORT, 587), 12 | secure: false, // Use SSL ou TLS (para ambientes de produção, certifique-se de usar "true" se o SMTP suportar SSL) 13 | auth: { 14 | user: process.env.EMAIL_USER, 15 | pass: process.env.EMAIL_PASS, 16 | }, 17 | }); 18 | } 19 | 20 | async sendEmail(to: string, subject: string, content: string) { 21 | const mailOptions = { 22 | from: `"Daqui" <${process.env.EMAIL_FROM}>`, // De quem está enviando 23 | to, // Destinatário 24 | subject, // Assunto 25 | // text: content, // Conteúdo do e-mail em texto simples 26 | // Se precisar enviar HTML, pode adicionar a propriedade "html" 27 | html: `

Recado: ${content}

`, 28 | }; 29 | 30 | await this.transporter.sendMail(mailOptions); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/recados/entities/recado.entity.ts: -------------------------------------------------------------------------------- 1 | import { Pessoa } from 'src/pessoas/entities/pessoa.entity'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | JoinColumn, 7 | ManyToOne, 8 | PrimaryGeneratedColumn, 9 | UpdateDateColumn, 10 | } from 'typeorm'; 11 | 12 | @Entity() 13 | export class Recado { 14 | @PrimaryGeneratedColumn() 15 | id: number; 16 | 17 | @Column({ type: 'varchar', length: 255 }) 18 | texto: string; 19 | 20 | @Column({ default: false }) 21 | lido: boolean; 22 | 23 | @Column() 24 | data: Date; // createdAt 25 | 26 | @CreateDateColumn() 27 | createdAt?: Date; // createdAt 28 | 29 | @UpdateDateColumn() 30 | updatedAt?: Date; // updatedAt 31 | 32 | // Muitos recados podem ser enviados por uma única pessoa (emissor) 33 | @ManyToOne(() => Pessoa, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) 34 | // Especifica a coluna "de" que armazena o ID da pessoa que enviou o recado 35 | @JoinColumn({ name: 'de' }) 36 | de: Pessoa; 37 | 38 | // Muitos recados podem ser enviados para uma única pessoa (destinatário) 39 | @ManyToOne(() => Pessoa, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) 40 | // Especifica a coluna "para" que armazena o ID da pessoa que recebe o recado 41 | @JoinColumn({ name: 'para' }) 42 | para: Pessoa; 43 | } 44 | -------------------------------------------------------------------------------- /src/pessoas/entities/pessoa.entity.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | import { Recado } from 'src/recados/entities/recado.entity'; 3 | import { 4 | Column, 5 | CreateDateColumn, 6 | Entity, 7 | OneToMany, 8 | PrimaryGeneratedColumn, 9 | UpdateDateColumn, 10 | } from 'typeorm'; 11 | 12 | @Entity() 13 | export class Pessoa { 14 | @PrimaryGeneratedColumn() 15 | id: number; 16 | 17 | @Column({ unique: true }) 18 | @IsEmail() 19 | email: string; 20 | 21 | @Column({ length: 255 }) 22 | passwordHash: string; 23 | 24 | @Column({ length: 100 }) 25 | nome: string; 26 | 27 | @CreateDateColumn() 28 | createdAt?: Date; 29 | 30 | @UpdateDateColumn() 31 | updatedAt?: Date; 32 | 33 | // Uma pessoa pode ter enviado muitos recados (como "de") 34 | // Esses recados são relacionados ao campo "de" na entidade recado 35 | @OneToMany(() => Recado, recado => recado.de) 36 | recadosEnviados: Recado[]; 37 | 38 | // Uma pessoa pode ter recebido muitos recados (como "para") 39 | // Esses recados são relacionados ao campo "para" na entidade recado 40 | @OneToMany(() => Recado, recado => recado.para) 41 | recadosRecebidos: Recado[]; 42 | 43 | @Column({ default: true }) 44 | active: boolean; 45 | 46 | @Column({ default: '' }) 47 | picture: string; 48 | 49 | // @Column({ type: 'simple-array', default: [] }) 50 | // routePolicies: RoutePolicies[]; 51 | } 52 | -------------------------------------------------------------------------------- /src/recados/dto/response-recado.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ReponseRecadoDto { 4 | @ApiProperty({ example: 1, description: 'ID único do recado' }) 5 | id: number; 6 | 7 | @ApiProperty({ 8 | example: 'Este é o conteúdo do recado', 9 | description: 'Texto do recado', 10 | }) 11 | texto: string; 12 | 13 | @ApiProperty({ example: true, description: 'Indica se o recado foi lido' }) 14 | lido: boolean; 15 | 16 | @ApiProperty({ 17 | example: '2024-09-14T10:00:00.000Z', 18 | description: 'Data do recado', 19 | }) 20 | data: Date; 21 | 22 | @ApiProperty({ 23 | example: '2024-09-10T12:34:56.000Z', 24 | description: 'Data de criação do recado', 25 | required: false, 26 | }) 27 | createdAt?: Date; 28 | 29 | @ApiProperty({ 30 | example: '2024-09-11T15:20:10.000Z', 31 | description: 'Data da última atualização do recado', 32 | required: false, 33 | }) 34 | updatedAt?: Date; 35 | 36 | @ApiProperty({ 37 | example: { id: 1, nome: 'João Silva' }, 38 | description: 'Informações sobre o remetente do recado', 39 | }) 40 | de: { 41 | id: number; 42 | nome: string; 43 | }; 44 | 45 | @ApiProperty({ 46 | example: { id: 2, nome: 'Maria Oliveira' }, 47 | description: 'Informações sobre o destinatário do recado', 48 | }) 49 | para: { 50 | id: number; 51 | nome: string; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/auth/guards/route-policy.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { Reflector } from '@nestjs/core'; 8 | import { REQUEST_TOKEN_PAYLOAD_KEY, ROUTE_POLICY_KEY } from '../auth.constants'; 9 | import { RoutePolicies } from '../enum/route-policies.enum'; 10 | import { Pessoa } from 'src/pessoas/entities/pessoa.entity'; 11 | 12 | @Injectable() 13 | export class RoutePolicyGuard implements CanActivate { 14 | constructor(private readonly reflector: Reflector) {} 15 | 16 | async canActivate(context: ExecutionContext): Promise { 17 | const routePolicyRequired = this.reflector.get( 18 | ROUTE_POLICY_KEY, 19 | context.getHandler(), 20 | ); 21 | 22 | // Não precisamos de permissões para essa rota 23 | // visto que nenhuma foi configurada 24 | if (!routePolicyRequired) { 25 | return true; 26 | } 27 | 28 | // Precisamos do tokenPayload vindo de AuthTokenGuard para continuar 29 | const request = context.switchToHttp().getRequest(); 30 | const tokenPayload = request[REQUEST_TOKEN_PAYLOAD_KEY]; 31 | 32 | if (!tokenPayload) { 33 | throw new UnauthorizedException( 34 | `Rota requer permissão ${routePolicyRequired}. Usuário não logado.`, 35 | ); 36 | } 37 | 38 | const { pessoa }: { pessoa: Pessoa } = tokenPayload; 39 | 40 | // if (!pessoa.routePolicies.includes(routePolicyRequired)) { 41 | // throw new UnauthorizedException( 42 | // `Usuário não tem permissão ${routePolicyRequired}`, 43 | // ); 44 | // } 45 | 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pessoas/dto/create-pessoa.dto.spec.ts: -------------------------------------------------------------------------------- 1 | import { validate } from 'class-validator'; 2 | import { CreatePessoaDto } from './create-pessoa.dto'; 3 | 4 | describe('CreatePessoaDto', () => { 5 | it('deve validar um DTO válido', async () => { 6 | const dto = new CreatePessoaDto(); 7 | dto.email = 'teste@example.com'; 8 | dto.password = 'senha123'; 9 | dto.nome = 'Luiz Otávio'; 10 | 11 | const errors = await validate(dto); 12 | expect(errors.length).toBe(0); // Nenhum erro significa que o DTO é válido 13 | }); 14 | 15 | it('deve falhar se o email for inválido', async () => { 16 | const dto = new CreatePessoaDto(); 17 | dto.email = 'email-invalido'; 18 | dto.password = 'senha123'; 19 | dto.nome = 'Luiz Otávio'; 20 | 21 | const errors = await validate(dto); 22 | 23 | expect(errors.length).toBe(1); 24 | expect(errors[0].property).toBe('email'); 25 | }); 26 | 27 | it('deve falhar se a senha for muito curta', async () => { 28 | const dto = new CreatePessoaDto(); 29 | dto.email = 'teste@example.com'; 30 | dto.password = '123'; 31 | dto.nome = 'Luiz Otávio'; 32 | 33 | const errors = await validate(dto); 34 | expect(errors.length).toBeGreaterThan(0); 35 | expect(errors[0].property).toBe('password'); 36 | }); 37 | 38 | it('deve falhar se o nome for vazio', async () => { 39 | const dto = new CreatePessoaDto(); 40 | dto.email = 'teste@example.com'; 41 | dto.password = 'senha123'; 42 | dto.nome = ''; 43 | 44 | const errors = await validate(dto); 45 | expect(errors.length).toBeGreaterThan(0); 46 | expect(errors[0].property).toBe('nome'); 47 | }); 48 | 49 | it('deve falhar se o nome for muito longo', async () => { 50 | const dto = new CreatePessoaDto(); 51 | dto.email = 'teste@example.com'; 52 | dto.password = 'senha123'; 53 | dto.nome = 'a'.repeat(101); 54 | 55 | const errors = await validate(dto); 56 | expect(errors.length).toBeGreaterThan(0); 57 | expect(errors[0].property).toBe('nome'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/auth/guards/auth-token.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Inject, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { JwtService } from '@nestjs/jwt'; 9 | import { Request } from 'express'; 10 | import jwtConfig from '../config/jwt.config'; 11 | import { ConfigType } from '@nestjs/config'; 12 | import { REQUEST_TOKEN_PAYLOAD_KEY } from '../auth.constants'; 13 | import { InjectRepository } from '@nestjs/typeorm'; 14 | import { Pessoa } from 'src/pessoas/entities/pessoa.entity'; 15 | import { Repository } from 'typeorm'; 16 | 17 | @Injectable() 18 | export class AuthTokenGuard implements CanActivate { 19 | constructor( 20 | @InjectRepository(Pessoa) 21 | private readonly pessoaRepository: Repository, 22 | private readonly jwtService: JwtService, 23 | @Inject(jwtConfig.KEY) 24 | private readonly jwtConfiguration: ConfigType, 25 | ) {} 26 | 27 | async canActivate(context: ExecutionContext): Promise { 28 | const request: Request = context.switchToHttp().getRequest(); 29 | const token = this.extractTokenFromHeader(request); 30 | 31 | if (!token) { 32 | throw new UnauthorizedException('Não logado!'); 33 | } 34 | 35 | try { 36 | const payload = await this.jwtService.verifyAsync( 37 | token, 38 | this.jwtConfiguration, 39 | ); 40 | 41 | const pessoa = await this.pessoaRepository.findOneBy({ 42 | id: payload.sub, 43 | active: true, 44 | }); 45 | 46 | if (!pessoa) { 47 | throw new UnauthorizedException('Pessoa não autorizada'); 48 | } 49 | 50 | payload['pessoa'] = pessoa; 51 | 52 | request[REQUEST_TOKEN_PAYLOAD_KEY] = payload; 53 | } catch (error) { 54 | throw new UnauthorizedException(error.message); 55 | } 56 | 57 | return true; 58 | } 59 | 60 | extractTokenFromHeader(request: Request): string | undefined { 61 | const authorization = request.headers?.authorization; 62 | 63 | if (!authorization || typeof authorization !== 'string') { 64 | return; 65 | } 66 | 67 | return authorization.split(' ')[1]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { RecadosModule } from 'src/recados/recados.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { PessoasModule } from 'src/pessoas/pessoas.module'; 7 | import { ConfigModule, ConfigType } from '@nestjs/config'; 8 | import globalConfig from 'src/global-config/global.config'; 9 | import { GlobalConfigModule } from 'src/global-config/global-config.module'; 10 | import { AuthModule } from 'src/auth/auth.module'; 11 | import { ServeStaticModule } from '@nestjs/serve-static'; 12 | import * as path from 'path'; 13 | import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; 14 | import { APP_GUARD } from '@nestjs/core'; 15 | 16 | @Module({ 17 | imports: [ 18 | ThrottlerModule.forRoot([ 19 | { 20 | ttl: 10000, // time to live em ms 21 | limit: 10, // máximo de requests durante o ttl 22 | blockDuration: 5000, // tempo de bloqueio 23 | }, 24 | ]), 25 | ConfigModule.forFeature(globalConfig), 26 | TypeOrmModule.forRootAsync({ 27 | imports: [ConfigModule.forFeature(globalConfig)], 28 | inject: [globalConfig.KEY], 29 | useFactory: async ( 30 | globalConfigurations: ConfigType, 31 | ) => { 32 | return { 33 | type: globalConfigurations.database.type, 34 | host: globalConfigurations.database.host, 35 | port: globalConfigurations.database.port, 36 | username: globalConfigurations.database.username, 37 | database: globalConfigurations.database.database, 38 | password: globalConfigurations.database.password, 39 | autoLoadEntities: globalConfigurations.database.autoLoadEntities, 40 | synchronize: globalConfigurations.database.synchronize, 41 | }; 42 | }, 43 | }), 44 | ServeStaticModule.forRoot({ 45 | rootPath: path.resolve(__dirname, '..', '..', 'pictures'), 46 | serveRoot: '/pictures', 47 | }), 48 | RecadosModule, 49 | PessoasModule, 50 | GlobalConfigModule, 51 | AuthModule, 52 | ], 53 | controllers: [AppController], 54 | providers: [ 55 | AppService, 56 | { 57 | provide: APP_GUARD, 58 | useClass: ThrottlerGuard, 59 | }, 60 | ], 61 | exports: [], 62 | }) 63 | export class AppModule {} 64 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "window.zoomLevel": 0, 3 | "editor.fontSize": 24, 4 | "terminal.integrated.fontSize": 24, 5 | "editor.suggestFontSize": 24, 6 | "debug.console.fontSize": 24, 7 | "editor.minimap.enabled": false, 8 | // Window 9 | "workbench.startupEditor": "newUntitledFile", 10 | "workbench.editor.labelFormat": "short", 11 | "workbench.editor.enablePreview": false, 12 | "workbench.list.smoothScrolling": true, 13 | "workbench.editor.enablePreviewFromQuickOpen": false, 14 | "workbench.editorAssociations": { 15 | "*.ipynb": "jupyter-notebook" 16 | }, 17 | "workbench.localHistory.enabled": true, 18 | "workbench.fontAliasing": "antialiased", 19 | "workbench.layoutControl.enabled": false, 20 | // Explorer 21 | "explorer.confirmDelete": true, 22 | "explorer.compactFolders": false, 23 | // Editor and breadcrumbs 24 | "breadcrumbs.enabled": true, 25 | "editor.largeFileOptimizations": true, 26 | "editor.renderControlCharacters": true, 27 | "editor.tabSize": 2, 28 | "editor.renderLineHighlight": "gutter", 29 | "editor.rulers": [ 30 | 79, 31 | 120 32 | ], 33 | "editor.detectIndentation": true, 34 | "editor.snippetSuggestions": "bottom", 35 | "editor.wordBasedSuggestions": "off", 36 | "editor.suggest.localityBonus": false, 37 | "editor.acceptSuggestionOnCommitCharacter": false, 38 | "editor.suggestSelection": "first", 39 | "editor.suggest.showValues": true, 40 | "editor.quickSuggestions": { 41 | "other": "on", 42 | "comments": "off", 43 | "strings": "off" 44 | }, 45 | "editor.quickSuggestionsDelay": 1, 46 | "editor.suggestOnTriggerCharacters": false, 47 | "editor.parameterHints.enabled": false, 48 | "editor.formatOnPaste": false, 49 | "editor.cursorSmoothCaretAnimation": "on", 50 | "editor.mouseWheelScrollSensitivity": 1, 51 | "editor.fastScrollSensitivity": 4, 52 | "editor.smoothScrolling": true, 53 | "editor.mouseWheelZoom": true, 54 | "editor.fontLigatures": true, 55 | "editor.formatOnSave": true, 56 | "editor.linkedEditing": true, 57 | "editor.multiCursorModifier": "ctrlCmd", 58 | "editor.accessibilitySupport": "off", 59 | "editor.renderWhitespace": "all", // gain space, but remove break points 60 | "editor.glyphMargin": true, 61 | "editor.inlineSuggest.enabled": true, 62 | // Git 63 | "git.enableSmartCommit": true, 64 | "git.openRepositoryInParentFolders": "never", 65 | "diffEditor.ignoreTrimWhitespace": true, 66 | // Eslint and fix on save 67 | "editor.codeActionsOnSave": { 68 | "source.fixAll.eslint": "explicit", 69 | "source.fixAll": "explicit" 70 | }, 71 | } -------------------------------------------------------------------------------- /client.rest: -------------------------------------------------------------------------------- 1 | @baseUrl = http://127.0.0.1:3000 2 | @authToken = {{authenticate.response.body.accessToken}} 3 | @refreshToken = {{authenticate.response.body.refreshToken}} 4 | 5 | # RECADOS 6 | ### Lista todos os recados 7 | GET {{baseUrl}}/recados/ 8 | ?limit=10 9 | &offset=0 10 | ### Lista apenas um recado 11 | GET {{baseUrl}}/recados/1/ 12 | ### Cria um recado 13 | POST {{baseUrl}}/recados/ 14 | Authorization: Bearer {{authToken}} 15 | Content-Type: application/json 16 | 17 | { 18 | "texto": "Este texto deve chegar no e-mail. \nAqui vem na próxima linha.", 19 | "paraId": 39 20 | } 21 | ### Atualiza um recado 22 | PATCH {{baseUrl}}/recados/70/ 23 | Authorization: Bearer {{authToken}} 24 | Content-Type: application/json 25 | 26 | { 27 | "texto": "De LUIZ para Joana", 28 | "lido": true 29 | } 30 | ### Apaga um recado 31 | DELETE {{baseUrl}}/recados/70/ 32 | Authorization: Bearer {{authToken}} 33 | 34 | 35 | # 36 | 37 | 38 | 39 | # Pessoas 40 | ### Lista todas as pessoas 41 | GET {{baseUrl}}/pessoas/ 42 | ?limit=10 43 | &offset=0 44 | Authorization: Bearer {{authToken}} 45 | ### Lista apenas uma pessoa 46 | GET {{baseUrl}}/pessoas/14/ 47 | Authorization: Bearer {{authToken}} 48 | ### Cria uma pessoa 49 | POST {{baseUrl}}/pessoas/ 50 | Content-Type: application/json 51 | 52 | { 53 | "email": "luiz@email.com", 54 | "password": "123456", 55 | "nome": "Luiz" 56 | } 57 | ### Atualiza uma pessoa 58 | PATCH {{baseUrl}}/pessoas/37/ 59 | Authorization: Bearer {{authToken}} 60 | Content-Type: application/json 61 | 62 | { 63 | "nome": "JOÃO" 64 | } 65 | ### Apaga uma pessoa 66 | DELETE {{baseUrl}}/pessoas/37/ 67 | Authorization: Bearer {{authToken}} 68 | 69 | 70 | 71 | # 72 | 73 | 74 | 75 | # AUTH 76 | ### Autenticação 77 | # @name authenticate 78 | POST {{baseUrl}}/auth/ 79 | Content-Type: application/json 80 | 81 | { 82 | "email": "luiz@email.com", 83 | "password": "123456" 84 | } 85 | ### Re-Autenticação 86 | # @name reAuthenticate 87 | POST {{baseUrl}}/auth/refresh/ 88 | Content-Type: application/json 89 | 90 | { 91 | "refreshToken": "{{refreshToken}}" 92 | } 93 | 94 | 95 | 96 | # 97 | 98 | 99 | 100 | # Upload 101 | ### Foto da pessoa 102 | POST {{baseUrl}}/pessoas/upload-picture/ 103 | Authorization: Bearer {{authToken}} 104 | Content-Type: multipart/form-data; boundary=----BoundaryDelimitadorHttp 105 | 106 | ------BoundaryDelimitadorHttp 107 | Content-Disposition: form-data; name="file"; filename="man.png" 108 | Content-Type: image/png 109 | 110 | < ./dev/images/man.png 111 | ------BoundaryDelimitadorHttp-- 112 | ### Ver foto 113 | GET {{baseUrl}}/pictures/39.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conceitos_nest", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@hapi/joi": "^17.1.1", 24 | "@nestjs/common": "^10.0.0", 25 | "@nestjs/config": "^3.2.3", 26 | "@nestjs/core": "^10.0.0", 27 | "@nestjs/jwt": "^10.2.0", 28 | "@nestjs/mapped-types": "^2.0.5", 29 | "@nestjs/platform-express": "^10.0.0", 30 | "@nestjs/serve-static": "^4.0.2", 31 | "@nestjs/swagger": "^7.4.0", 32 | "@nestjs/throttler": "^6.2.1", 33 | "@nestjs/typeorm": "^10.0.2", 34 | "bcryptjs": "^2.4.3", 35 | "class-transformer": "^0.5.1", 36 | "class-validator": "^0.14.1", 37 | "helmet": "^7.1.0", 38 | "nodemailer": "^6.9.15", 39 | "pg": "^8.12.0", 40 | "reflect-metadata": "^0.2.0", 41 | "rxjs": "^7.8.1", 42 | "swagger-ui-express": "^5.0.1", 43 | "typeorm": "^0.3.20" 44 | }, 45 | "devDependencies": { 46 | "@nestjs/cli": "^10.0.0", 47 | "@nestjs/schematics": "^10.0.0", 48 | "@nestjs/testing": "^10.0.0", 49 | "@types/bcryptjs": "^2.4.6", 50 | "@types/express": "^4.17.17", 51 | "@types/hapi__joi": "^17.1.14", 52 | "@types/jest": "^29.5.2", 53 | "@types/multer": "^1.4.12", 54 | "@types/node": "^20.3.1", 55 | "@types/nodemailer": "^6.4.15", 56 | "@types/supertest": "^6.0.0", 57 | "@typescript-eslint/eslint-plugin": "^7.0.0", 58 | "@typescript-eslint/parser": "^7.0.0", 59 | "eslint": "^8.42.0", 60 | "eslint-config-prettier": "^9.0.0", 61 | "eslint-plugin-prettier": "^5.0.0", 62 | "jest": "^29.5.0", 63 | "prettier": "^3.0.0", 64 | "source-map-support": "^0.5.21", 65 | "supertest": "^7.0.0", 66 | "ts-jest": "^29.1.0", 67 | "ts-loader": "^9.4.3", 68 | "ts-node": "^10.9.1", 69 | "tsconfig-paths": "^4.2.0", 70 | "typescript": "^5.1.3" 71 | }, 72 | "jest": { 73 | "moduleFileExtensions": [ 74 | "js", 75 | "json", 76 | "ts" 77 | ], 78 | "rootDir": "src", 79 | "testRegex": ".*\\.spec\\.ts$", 80 | "transform": { 81 | "^.+\\.(t|j)s$": "ts-jest" 82 | }, 83 | "collectCoverageFrom": [ 84 | "**/*.(t|j)s" 85 | ], 86 | "coverageDirectory": "../coverage", 87 | "testEnvironment": "node", 88 | "moduleNameMapper": { 89 | "^src/(.*)$": "/$1" 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/pessoas/pessoas.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseGuards, 10 | UseInterceptors, 11 | UploadedFile, 12 | ParseFilePipeBuilder, 13 | HttpStatus, 14 | } from '@nestjs/common'; 15 | import { PessoasService } from './pessoas.service'; 16 | import { CreatePessoaDto } from './dto/create-pessoa.dto'; 17 | import { UpdatePessoaDto } from './dto/update-pessoa.dto'; 18 | import { AuthTokenGuard } from 'src/auth/guards/auth-token.guard'; 19 | import { TokenPayloadParam } from 'src/auth/params/token-payload.param'; 20 | import { TokenPayloadDto } from 'src/auth/dto/token-payload.dto'; 21 | import { FileInterceptor } from '@nestjs/platform-express'; 22 | import { ApiBearerAuth, ApiBody, ApiConsumes } from '@nestjs/swagger'; 23 | 24 | @Controller('pessoas') 25 | export class PessoasController { 26 | constructor(private readonly pessoasService: PessoasService) {} 27 | 28 | @Post() 29 | create(@Body() createPessoaDto: CreatePessoaDto) { 30 | return this.pessoasService.create(createPessoaDto); 31 | } 32 | 33 | @UseGuards(AuthTokenGuard) 34 | @ApiBearerAuth() 35 | @Get() 36 | findAll() { 37 | return this.pessoasService.findAll(); 38 | } 39 | 40 | @UseGuards(AuthTokenGuard) 41 | @ApiBearerAuth() 42 | @Get(':id') 43 | findOne(@Param('id') id: string) { 44 | return this.pessoasService.findOne(+id); 45 | } 46 | 47 | @UseGuards(AuthTokenGuard) 48 | @ApiBearerAuth() 49 | @Patch(':id') 50 | update( 51 | @Param('id') id: string, 52 | @Body() updatePessoaDto: UpdatePessoaDto, 53 | @TokenPayloadParam() tokenPayload: TokenPayloadDto, 54 | ) { 55 | return this.pessoasService.update(+id, updatePessoaDto, tokenPayload); 56 | } 57 | 58 | @UseGuards(AuthTokenGuard) 59 | @ApiBearerAuth() 60 | @Delete(':id') 61 | remove( 62 | @Param('id') id: string, 63 | @TokenPayloadParam() tokenPayload: TokenPayloadDto, 64 | ) { 65 | return this.pessoasService.remove(+id, tokenPayload); 66 | } 67 | 68 | @UseGuards(AuthTokenGuard) 69 | @ApiBearerAuth() 70 | @ApiConsumes('multipart/form-data') // Indica que o endpoint consome dados multipart 71 | @ApiBody({ 72 | schema: { 73 | type: 'object', 74 | properties: { 75 | file: { 76 | type: 'string', 77 | format: 'binary', 78 | }, 79 | }, 80 | }, 81 | }) // Indica que esperamos um arquivo no campo file e o formato é binário 82 | @UseInterceptors(FileInterceptor('file')) 83 | @Post('upload-picture') 84 | async uploadPicture( 85 | @UploadedFile( 86 | new ParseFilePipeBuilder() 87 | .addFileTypeValidator({ 88 | fileType: /jpeg|jpg|png/g, 89 | }) 90 | .addMaxSizeValidator({ 91 | maxSize: 10 * (1024 * 1024), 92 | }) 93 | .build({ 94 | errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, 95 | }), 96 | ) 97 | file: Express.Multer.File, 98 | @TokenPayloadParam() tokenPayload: TokenPayloadDto, 99 | ) { 100 | return this.pessoasService.uploadPicture(file, tokenPayload); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { LoginDto } from './dto/login.dto'; 3 | import { Repository } from 'typeorm'; 4 | import { Pessoa } from 'src/pessoas/entities/pessoa.entity'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { HashingService } from './hashing/hashing.service'; 7 | import jwtConfig from './config/jwt.config'; 8 | import { ConfigType } from '@nestjs/config'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import { RefreshTokenDto } from './dto/refresh-token.dto'; 11 | 12 | @Injectable() 13 | export class AuthService { 14 | constructor( 15 | @InjectRepository(Pessoa) 16 | private readonly pessoaRepository: Repository, 17 | private readonly hashingService: HashingService, 18 | @Inject(jwtConfig.KEY) 19 | private readonly jwtConfiguration: ConfigType, 20 | private readonly jwtService: JwtService, 21 | ) {} 22 | 23 | async login(loginDto: LoginDto) { 24 | const pessoa = await this.pessoaRepository.findOneBy({ 25 | email: loginDto.email, 26 | active: true, 27 | }); 28 | 29 | if (!pessoa) { 30 | throw new UnauthorizedException('Pessoa não autorizada'); 31 | } 32 | 33 | const passwordIsValid = await this.hashingService.compare( 34 | loginDto.password, 35 | pessoa.passwordHash, 36 | ); 37 | 38 | if (!passwordIsValid) { 39 | throw new UnauthorizedException('Senha inválida!'); 40 | } 41 | 42 | return this.createTokens(pessoa); 43 | } 44 | 45 | private async createTokens(pessoa: Pessoa) { 46 | const accessTokenPromise = this.signJwtAsync>( 47 | pessoa.id, 48 | this.jwtConfiguration.jwtTtl, 49 | { email: pessoa.email }, 50 | ); 51 | 52 | const refreshTokenPromise = this.signJwtAsync( 53 | pessoa.id, 54 | this.jwtConfiguration.jwtRefreshTtl, 55 | ); 56 | 57 | const [accessToken, refreshToken] = await Promise.all([ 58 | accessTokenPromise, 59 | refreshTokenPromise, 60 | ]); 61 | 62 | return { 63 | accessToken, 64 | refreshToken, 65 | }; 66 | } 67 | 68 | private async signJwtAsync(sub: number, expiresIn: number, payload?: T) { 69 | return await this.jwtService.signAsync( 70 | { 71 | sub, 72 | ...payload, 73 | }, 74 | { 75 | audience: this.jwtConfiguration.audience, 76 | issuer: this.jwtConfiguration.issuer, 77 | secret: this.jwtConfiguration.secret, 78 | expiresIn, 79 | }, 80 | ); 81 | } 82 | 83 | async refreshTokens(refreshTokenDto: RefreshTokenDto) { 84 | try { 85 | const { sub } = await this.jwtService.verifyAsync( 86 | refreshTokenDto.refreshToken, 87 | this.jwtConfiguration, 88 | ); 89 | 90 | const pessoa = await this.pessoaRepository.findOneBy({ 91 | id: sub, 92 | active: true, 93 | }); 94 | 95 | if (!pessoa) { 96 | throw new Error('Pessoa não autorizada'); 97 | } 98 | 99 | return this.createTokens(pessoa); 100 | } catch (error) { 101 | throw new UnauthorizedException(error.message); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /src/pessoas/pessoas.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { PessoasController } from './pessoas.controller'; 2 | 3 | describe('PessoasController', () => { 4 | let controller: PessoasController; 5 | const pessoasServiceMock = { 6 | create: jest.fn(), 7 | findAll: jest.fn(), 8 | findOne: jest.fn(), 9 | update: jest.fn(), 10 | remove: jest.fn(), 11 | uploadPicture: jest.fn(), 12 | }; 13 | 14 | beforeEach(() => { 15 | controller = new PessoasController(pessoasServiceMock as any); 16 | }); 17 | 18 | it('create - deve usar o PessoaService com o argumento correto', async () => { 19 | const argument = { key: 'value' }; 20 | const expected = { anyKey: 'anyValue' }; 21 | 22 | jest.spyOn(pessoasServiceMock, 'create').mockResolvedValue(expected); 23 | 24 | const result = await controller.create(argument as any); 25 | 26 | expect(pessoasServiceMock.create).toHaveBeenCalledWith(argument); 27 | expect(result).toEqual(expected); 28 | }); 29 | 30 | it('findAll - deve usar o PessoaService', async () => { 31 | const expected = { anyKey: 'anyValue' }; 32 | 33 | jest.spyOn(pessoasServiceMock, 'findAll').mockResolvedValue(expected); 34 | 35 | const result = await controller.findAll(); 36 | 37 | expect(pessoasServiceMock.create).toHaveBeenCalled(); 38 | expect(result).toEqual(expected); 39 | }); 40 | 41 | it('findOne - deve usar o PessoaService com o argumento correto', async () => { 42 | const argument = '1'; 43 | const expected = { anyKey: 'anyValue' }; 44 | 45 | jest.spyOn(pessoasServiceMock, 'findOne').mockResolvedValue(expected); 46 | 47 | const result = await controller.findOne(argument as any); 48 | 49 | expect(pessoasServiceMock.findOne).toHaveBeenCalledWith(+argument); 50 | expect(result).toEqual(expected); 51 | }); 52 | 53 | it('update - deve usar o PessoaService com os argumentos corretos', async () => { 54 | const argument1 = '1'; 55 | const argument2 = { key: 'value' }; 56 | const argument3 = { key: 'value' }; 57 | const expected = { anyKey: 'anyValue' }; 58 | 59 | jest.spyOn(pessoasServiceMock, 'update').mockResolvedValue(expected); 60 | 61 | const result = await controller.update( 62 | argument1, 63 | argument2 as any, 64 | argument3 as any, 65 | ); 66 | 67 | expect(pessoasServiceMock.update).toHaveBeenCalledWith( 68 | +argument1, 69 | argument2, 70 | argument3, 71 | ); 72 | expect(result).toEqual(expected); 73 | }); 74 | 75 | it('remove - deve usar o PessoaService com os argumentos corretos', async () => { 76 | const argument1 = 1; 77 | const argument2 = { aKey: 'aValue' }; 78 | const expected = { anyKey: 'anyValue' }; 79 | 80 | jest.spyOn(pessoasServiceMock, 'remove').mockResolvedValue(expected); 81 | 82 | const result = await controller.remove(argument1 as any, argument2 as any); 83 | 84 | expect(pessoasServiceMock.remove).toHaveBeenCalledWith( 85 | +argument1, 86 | argument2, 87 | ); 88 | expect(result).toEqual(expected); 89 | }); 90 | 91 | it('uploadPicture - deve usar o PessoaService com os argumentos corretos', async () => { 92 | const argument1 = { aKey: 'aValue' }; 93 | const argument2 = { bKey: 'bValue' }; 94 | const expected = { anyKey: 'anyValue' }; 95 | 96 | jest.spyOn(pessoasServiceMock, 'uploadPicture').mockResolvedValue(expected); 97 | 98 | const result = await controller.uploadPicture( 99 | argument1 as any, 100 | argument2 as any, 101 | ); 102 | 103 | expect(pessoasServiceMock.uploadPicture).toHaveBeenCalledWith( 104 | argument1, 105 | argument2, 106 | ); 107 | expect(result).toEqual(expected); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/pessoas/pessoas.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | ConflictException, 4 | ForbiddenException, 5 | Injectable, 6 | NotFoundException, 7 | Scope, 8 | } from '@nestjs/common'; 9 | import { CreatePessoaDto } from './dto/create-pessoa.dto'; 10 | import { UpdatePessoaDto } from './dto/update-pessoa.dto'; 11 | import { InjectRepository } from '@nestjs/typeorm'; 12 | import { Pessoa } from './entities/pessoa.entity'; 13 | import { Repository } from 'typeorm'; 14 | import { HashingService } from 'src/auth/hashing/hashing.service'; 15 | import { TokenPayloadDto } from 'src/auth/dto/token-payload.dto'; 16 | import * as path from 'path'; 17 | import * as fs from 'fs/promises'; 18 | 19 | @Injectable({ scope: Scope.DEFAULT }) 20 | export class PessoasService { 21 | constructor( 22 | @InjectRepository(Pessoa) 23 | private readonly pessoaRepository: Repository, 24 | private readonly hashingService: HashingService, 25 | ) {} 26 | 27 | async create(createPessoaDto: CreatePessoaDto) { 28 | try { 29 | const passwordHash = await this.hashingService.hash( 30 | createPessoaDto.password, 31 | ); 32 | 33 | const dadosPessoa = { 34 | nome: createPessoaDto.nome, 35 | passwordHash, 36 | email: createPessoaDto.email, 37 | // routePolicies: createPessoaDto.routePolicies, 38 | }; 39 | 40 | const novaPessoa = this.pessoaRepository.create(dadosPessoa); 41 | await this.pessoaRepository.save(novaPessoa); 42 | return novaPessoa; 43 | } catch (error) { 44 | if (error.code === '23505') { 45 | throw new ConflictException('E-mail já está cadastrado.'); 46 | } 47 | 48 | throw error; 49 | } 50 | } 51 | 52 | async findAll() { 53 | const pessoas = await this.pessoaRepository.find({ 54 | order: { 55 | id: 'desc', 56 | }, 57 | }); 58 | 59 | return pessoas; 60 | } 61 | 62 | async findOne(id: number) { 63 | const pessoa = await this.pessoaRepository.findOneBy({ 64 | id, 65 | }); 66 | 67 | if (!pessoa) { 68 | throw new NotFoundException('Pessoa não encontrada'); 69 | } 70 | 71 | return pessoa; 72 | } 73 | 74 | async update( 75 | id: number, 76 | updatePessoaDto: UpdatePessoaDto, 77 | tokenPayload: TokenPayloadDto, 78 | ) { 79 | const dadosPessoa = { 80 | nome: updatePessoaDto?.nome, 81 | }; 82 | 83 | if (updatePessoaDto?.password) { 84 | const passwordHash = await this.hashingService.hash( 85 | updatePessoaDto.password, 86 | ); 87 | 88 | dadosPessoa['passwordHash'] = passwordHash; 89 | } 90 | 91 | const pessoa = await this.pessoaRepository.preload({ 92 | id, 93 | ...dadosPessoa, 94 | }); 95 | 96 | if (!pessoa) { 97 | throw new NotFoundException('Pessoa não encontrada'); 98 | } 99 | 100 | if (pessoa.id !== tokenPayload.sub) { 101 | throw new ForbiddenException('Você não é essa pessoa.'); 102 | } 103 | 104 | return this.pessoaRepository.save(pessoa); 105 | } 106 | 107 | async remove(id: number, tokenPayload: TokenPayloadDto) { 108 | const pessoa = await this.findOne(id); 109 | 110 | if (pessoa.id !== tokenPayload.sub) { 111 | throw new ForbiddenException('Você não é essa pessoa.'); 112 | } 113 | 114 | return this.pessoaRepository.remove(pessoa); 115 | } 116 | 117 | async uploadPicture( 118 | file: Express.Multer.File, 119 | tokenPayload: TokenPayloadDto, 120 | ) { 121 | if (file.size < 1024) { 122 | throw new BadRequestException('File too small'); 123 | } 124 | 125 | const pessoa = await this.findOne(tokenPayload.sub); 126 | 127 | const fileExtension = path 128 | .extname(file.originalname) 129 | .toLowerCase() 130 | .substring(1); 131 | const fileName = `${tokenPayload.sub}.${fileExtension}`; 132 | const fileFullPath = path.resolve(process.cwd(), 'pictures', fileName); 133 | 134 | await fs.writeFile(fileFullPath, file.buffer); 135 | 136 | pessoa.picture = fileName; 137 | await this.pessoaRepository.save(pessoa); 138 | 139 | return pessoa; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/recados/recados.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ForbiddenException, 3 | Injectable, 4 | NotFoundException, 5 | Scope, 6 | } from '@nestjs/common'; 7 | import { Recado } from './entities/recado.entity'; 8 | import { CreateRecadoDto } from './dto/create-recado.dto'; 9 | import { UpdateRecadoDto } from './dto/update-recado.dto'; 10 | import { InjectRepository } from '@nestjs/typeorm'; 11 | import { Repository } from 'typeorm'; 12 | import { PessoasService } from 'src/pessoas/pessoas.service'; 13 | import { PaginationDto } from 'src/common/dto/pagination.dto'; 14 | import { TokenPayloadDto } from 'src/auth/dto/token-payload.dto'; 15 | import { EmailService } from 'src/email/email.service'; 16 | import { ReponseRecadoDto } from './dto/response-recado.dto'; 17 | 18 | @Injectable({ scope: Scope.DEFAULT }) 19 | export class RecadosService { 20 | private count = 0; 21 | 22 | constructor( 23 | @InjectRepository(Recado) 24 | private readonly recadoRepository: Repository, 25 | private readonly pessoasService: PessoasService, 26 | private readonly emailService: EmailService, 27 | ) {} 28 | 29 | throwNotFoundError() { 30 | throw new NotFoundException('Recado não encontrado'); 31 | } 32 | 33 | async findAll(paginationDto?: PaginationDto): Promise { 34 | const { limit = 10, offset = 0 } = paginationDto; 35 | 36 | const recados = await this.recadoRepository.find({ 37 | take: limit, // quantos registros serão exibidos (por página) 38 | skip: offset, // quantos registros devem ser pulados 39 | relations: ['de', 'para'], 40 | order: { 41 | id: 'desc', 42 | }, 43 | select: { 44 | de: { 45 | id: true, 46 | nome: true, 47 | }, 48 | para: { 49 | id: true, 50 | nome: true, 51 | }, 52 | }, 53 | }); 54 | 55 | return recados; 56 | } 57 | 58 | async findOne(id: number): Promise { 59 | // const recado = this.recados.find(item => item.id === id); 60 | const recado = await this.recadoRepository.findOne({ 61 | where: { 62 | id, 63 | }, 64 | relations: ['de', 'para'], 65 | order: { 66 | id: 'desc', 67 | }, 68 | select: { 69 | de: { 70 | id: true, 71 | nome: true, 72 | }, 73 | para: { 74 | id: true, 75 | nome: true, 76 | }, 77 | }, 78 | }); 79 | 80 | if (recado) return recado; 81 | 82 | this.throwNotFoundError(); 83 | } 84 | 85 | async create( 86 | createRecadoDto: CreateRecadoDto, 87 | tokenPayload: TokenPayloadDto, 88 | ): Promise { 89 | const { paraId } = createRecadoDto; 90 | 91 | // Encontrar a pessoa para quem o recado está sendo enviado 92 | const para = await this.pessoasService.findOne(paraId); 93 | 94 | // Encontrar a pessoa que está criando o recado 95 | const de = await this.pessoasService.findOne(tokenPayload.sub); 96 | 97 | const novoRecado = { 98 | texto: createRecadoDto.texto, 99 | de, 100 | para, 101 | lido: false, 102 | data: new Date(), 103 | }; 104 | 105 | const recado = this.recadoRepository.create(novoRecado); 106 | await this.recadoRepository.save(recado); 107 | 108 | // await this.emailService.sendEmail( 109 | // para.email, 110 | // `Você recebeu um recado de "${de.nome}" <${de.email}>`, 111 | // createRecadoDto.texto, 112 | // ); 113 | 114 | return { 115 | ...recado, 116 | de: { 117 | id: recado.de.id, 118 | nome: recado.de.nome, 119 | }, 120 | para: { 121 | id: recado.para.id, 122 | nome: recado.para.nome, 123 | }, 124 | }; 125 | } 126 | 127 | async update( 128 | id: number, 129 | updateRecadoDto: UpdateRecadoDto, 130 | tokenPayload: TokenPayloadDto, 131 | ): Promise { 132 | const recado = await this.findOne(id); 133 | 134 | if (recado.de.id !== tokenPayload.sub) { 135 | throw new ForbiddenException('Esse recado não é seu'); 136 | } 137 | 138 | recado.texto = updateRecadoDto?.texto ?? recado.texto; 139 | recado.lido = updateRecadoDto?.lido ?? recado.lido; 140 | 141 | await this.recadoRepository.save(recado); 142 | return recado; 143 | } 144 | 145 | async remove( 146 | id: number, 147 | tokenPayload: TokenPayloadDto, 148 | ): Promise { 149 | const recado = await this.findOne(id); 150 | 151 | if (recado.de.id !== tokenPayload.sub) { 152 | throw new ForbiddenException('Esse recado não é seu'); 153 | } 154 | 155 | await this.recadoRepository.delete(recado.id); 156 | 157 | return recado; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/recados/recados.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Controller, 5 | Delete, 6 | Get, 7 | Param, 8 | Patch, 9 | Post, 10 | Query, 11 | UseGuards, 12 | } from '@nestjs/common'; 13 | import { RecadosService } from './recados.service'; 14 | import { CreateRecadoDto } from './dto/create-recado.dto'; 15 | import { UpdateRecadoDto } from './dto/update-recado.dto'; 16 | import { PaginationDto } from 'src/common/dto/pagination.dto'; 17 | import { TokenPayloadParam } from 'src/auth/params/token-payload.param'; 18 | import { TokenPayloadDto } from 'src/auth/dto/token-payload.dto'; 19 | import { AuthTokenGuard } from 'src/auth/guards/auth-token.guard'; 20 | import { 21 | ApiBearerAuth, 22 | ApiTags, 23 | ApiOperation, 24 | ApiResponse, 25 | ApiQuery, 26 | ApiParam, 27 | } from '@nestjs/swagger'; 28 | import { ReponseRecadoDto } from './dto/response-recado.dto'; 29 | 30 | @ApiTags('recados') // Tag usada para organizar os endpoints 31 | @Controller('recados') 32 | export class RecadosController { 33 | constructor(private readonly recadosService: RecadosService) {} 34 | 35 | @Get() 36 | @ApiOperation({ summary: 'Obter todos os recados com paginação' }) // Descrição do endpoint 37 | @ApiQuery({ 38 | name: 'offset', 39 | required: false, 40 | example: 1, 41 | description: 'Itens a pular', 42 | }) // Parâmetros da query 43 | @ApiQuery({ 44 | name: 'limit', 45 | required: false, 46 | example: 10, 47 | description: 'Limite de itens por página', 48 | }) 49 | @ApiResponse({ 50 | status: 200, 51 | description: 'Recados retornados com sucesso.', 52 | type: [ReponseRecadoDto], 53 | }) // Resposta bem-sucedida 54 | async findAll(@Query() paginationDto: PaginationDto) { 55 | const recados = await this.recadosService.findAll(paginationDto); 56 | return recados; 57 | } 58 | 59 | @Get(':id') 60 | @ApiOperation({ summary: 'Obter um recado específico pelo ID' }) // Descrição da operação 61 | @ApiParam({ name: 'id', description: 'ID do recado', example: 1 }) // Parâmetro da rota 62 | @ApiResponse({ 63 | status: 200, 64 | description: 'Recado retornado com sucesso.', 65 | type: ReponseRecadoDto, 66 | }) // Resposta bem-sucedida 67 | @ApiResponse({ status: 404, description: 'Recado não encontrado.' }) // Resposta de erro 68 | findOne(@Param('id') id: string) { 69 | return this.recadosService.findOne(+id); 70 | } 71 | 72 | @UseGuards(AuthTokenGuard) 73 | @ApiBearerAuth() // Autenticação via token 74 | @Post() 75 | @ApiOperation({ summary: 'Criar um novo recado' }) // Descrição do endpoint 76 | @ApiResponse({ 77 | status: 201, 78 | description: 'Recado criado com sucesso.', 79 | type: ReponseRecadoDto, 80 | // example: { 81 | // id: 19, 82 | // texto: 'EXEMPLO', 83 | // lido: true, 84 | // data: '2024-09-14T10:00:00.000Z', 85 | // createdAt: '2024-09-10T12:34:56.000Z', 86 | // updatedAt: '2024-09-11T15:20:10.000Z', 87 | // de: { 88 | // id: 0, 89 | // nome: 'string', 90 | // }, 91 | // para: { 92 | // id: 0, 93 | // nome: 'string', 94 | // }, 95 | // }, 96 | }) // Resposta de criação bem-sucedida 97 | @ApiResponse({ 98 | status: 400, 99 | description: 'Dados inválidos.', 100 | type: BadRequestException, 101 | example: new BadRequestException('Error message').getResponse(), 102 | }) // Resposta de erro 103 | create( 104 | @Body() createRecadoDto: CreateRecadoDto, 105 | @TokenPayloadParam() tokenPayload: TokenPayloadDto, 106 | ) { 107 | return this.recadosService.create(createRecadoDto, tokenPayload); 108 | } 109 | 110 | @UseGuards(AuthTokenGuard) 111 | @ApiBearerAuth() 112 | @Patch(':id') 113 | @ApiOperation({ summary: 'Atualizar um recado existente' }) // Descrição da operação 114 | @ApiParam({ name: 'id', description: 'ID do recado', example: 1 }) // Parâmetro da rota 115 | @ApiResponse({ status: 200, description: 'Recado atualizado com sucesso.' }) // Resposta de sucesso 116 | @ApiResponse({ status: 404, description: 'Recado não encontrado.' }) // Resposta de erro 117 | update( 118 | @Param('id') id: number, 119 | @Body() updateRecadoDto: UpdateRecadoDto, 120 | @TokenPayloadParam() tokenPayload: TokenPayloadDto, 121 | ) { 122 | return this.recadosService.update(id, updateRecadoDto, tokenPayload); 123 | } 124 | 125 | @UseGuards(AuthTokenGuard) 126 | @ApiBearerAuth() 127 | @Delete(':id') 128 | @ApiOperation({ summary: 'Excluir um recado' }) // Descrição do endpoint 129 | @ApiParam({ name: 'id', description: 'ID do recado', example: 1 }) // Parâmetro da rota 130 | @ApiResponse({ status: 200, description: 'Recado excluído com sucesso.' }) // Resposta de sucesso 131 | @ApiResponse({ status: 404, description: 'Recado não encontrado.' }) // Resposta de erro 132 | remove( 133 | @Param('id') id: number, 134 | @TokenPayloadParam() tokenPayload: TokenPayloadDto, 135 | ) { 136 | return this.recadosService.remove(id, tokenPayload); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /SERVER.md: -------------------------------------------------------------------------------- 1 | ``` 2 | !!! ATENÇÃO !!! 3 | LEMBRETE package-lock.json 4 | 5 | # DOMÍNIO 6 | 7 | # SERVIDOR 8 | sudo apt update -y 9 | sudo apt upgrade -y 10 | 11 | sudo apt install git curl 12 | sudo apt install postgresql 13 | 14 | 15 | # POSTGRESQL 16 | sudo -u postgres psql 17 | 18 | create user USUARIO with encrypted password 'SENHA'; 19 | CREATE DATABASE NOMEDABASE WITH OWNER USUARIO; 20 | GRANT ALL PRIVILEGES ON DATABASE NOMEDABASE TO USUARIO; 21 | 22 | sudo systemctl restart postgresql 23 | 24 | 25 | # NODE 26 | # installs nvm (Node Version Manager) 27 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash 28 | 29 | # download and install Node.js (you may need to restart the terminal) 30 | nvm install 20 31 | 32 | !!! ATENÇÃO !!! 33 | LEMBRETE package-lock.json 34 | 35 | 36 | # BAIXAR O CÓDIGO DO GITHUB 37 | git clone ENDERECO_GITHUN_SSH 38 | 39 | 40 | # .env 41 | DATABASE_TYPE='postgres' 42 | DATABASE_HOST='localhost' 43 | DATABASE_PORT=5432 44 | DATABASE_USERNAME='nestjs' 45 | DATABASE_DATABASE='nestjs' 46 | DATABASE_PASSWORD='nestjs' 47 | DATABASE_AUTOLOADENTITIES=1 48 | DATABASE_SYNCHRONIZE=1 49 | 50 | JWT_SECRET=1d13afba3b3835d8e7a475803cf75f3f9ff9907d6b3ff29f2b4f27037cae2f2f0e760ea2797ed3fa2b15af9f1cd5f1f20d2be6c98370bd2fc130955660e1095ce790013ceefa49e47c3c5816214f02e494fe1472f1a378866272767f63b503d72365081a7a626ccc85a9cf04447a0785cfb48a35583ebb10b50e02c0e68b5932a871538247f3ae889911949126a3813ac416d46c63869098fa4438fba3e59aed90e947659d0edc2adcc64dac1dad4f2c303352153c9eab11abc9d68b077ab7aadf5c5aa11cbecd87482f6043a071842aebd92898506e15b404464832426ead2fceb3f13a1362d641b693d8e267db51972f7ac36cb241a3014a6d77c73ff26250 51 | JWT_TOKEN_AUDIENCE=http://localhost:3000 52 | JWT_TOKEN_ISSUER=http://localhost:3000 53 | JWT_TTL=3600 54 | JWT_REFRESH_TTL=86400 55 | 56 | APP_PORT=3000 57 | NODE_ENV='production' 58 | 59 | !!! ATENÇÃO !!! 60 | MUDAR .ENV 61 | DATABASE_SYNCHRONIZE=0 62 | 63 | 64 | # BUILD 65 | npm run build 66 | 67 | 68 | # LETSENCRYPT 69 | sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048 70 | sudo apt-get install certbot 71 | sudo service nginx stop 72 | sudo certbot certonly --standalone -d SEU_DOMINIO 73 | sudo service nginx start 74 | 75 | 76 | 77 | 78 | # NGINX 79 | sudo apt install nginx 80 | sudo nano /etc/nginx/sites-available/nestjs.otaviomiranda.com.br 81 | 82 | 83 | # Redireciona para HTTPS 84 | server { 85 | listen 80; 86 | listen [::]:80; 87 | server_name nestjs.otaviomiranda.com.br; 88 | return 301 https://$host$request_uri; 89 | } 90 | 91 | # HTTPS 92 | server { 93 | listen 443 ssl http2; 94 | listen [::]:443 ssl http2; 95 | 96 | server_name nestjs.otaviomiranda.com.br; 97 | 98 | # O servidor só vai responder pra este domínio 99 | if ($host != "nestjs.otaviomiranda.com.br") { 100 | return 404; 101 | } 102 | 103 | ssl_certificate /etc/letsencrypt/live/nestjs.otaviomiranda.com.br/fullchain.pem; # managed by Certbot 104 | ssl_certificate_key /etc/letsencrypt/live/nestjs.otaviomiranda.com.br/privkey.pem; # managed by Certbot 105 | ssl_trusted_certificate /etc/letsencrypt/live/nestjs.otaviomiranda.com.br/chain.pem; 106 | 107 | # Improve HTTPS performance with session resumption 108 | ssl_session_cache shared:SSL:10m; 109 | ssl_session_timeout 5m; 110 | 111 | # Enable server-side protection against BEAST attacks 112 | ssl_prefer_server_ciphers on; 113 | ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5; 114 | 115 | # Disable SSLv3 116 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 117 | 118 | # Diffie-Hellman parameter for DHE ciphersuites 119 | # $ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 120 | ssl_dhparam /etc/ssl/certs/dhparam.pem; 121 | 122 | # Enable HSTS (https://developer.mozilla.org/en-US/docs/Security/HTTP_Strict_Transport_Security) 123 | add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; 124 | 125 | # Enable OCSP stapling (http://blog.mozilla.org/security/2013/07/29/ocsp-stapling-in-firefox) 126 | ssl_stapling on; 127 | ssl_stapling_verify on; 128 | resolver 8.8.8.8 8.8.4.4 valid=300s; 129 | resolver_timeout 5s; 130 | 131 | # Add index.php to the list if you are using PHP 132 | index index.html index.htm index.nginx-debian.html index.php; 133 | 134 | location / { 135 | proxy_pass http://localhost:3000; 136 | proxy_http_version 1.1; 137 | proxy_set_header Upgrade $http_upgrade; 138 | proxy_set_header Connection 'upgrade'; 139 | proxy_set_header Host $host; 140 | proxy_cache_bypass $http_upgrade; 141 | } 142 | 143 | # deny access to .htaccess files, if Apache's document root 144 | # concurs with nginx's one 145 | # 146 | location ~ /\.ht { 147 | deny all; 148 | } 149 | 150 | location ~ /\. { 151 | access_log off; 152 | log_not_found off; 153 | deny all; 154 | } 155 | 156 | gzip on; 157 | gzip_disable "msie6"; 158 | 159 | gzip_comp_level 6; 160 | gzip_min_length 1100; 161 | gzip_buffers 4 32k; 162 | gzip_proxied any; 163 | gzip_types 164 | text/plain 165 | text/css 166 | text/js 167 | text/xml 168 | text/javascript 169 | application/javascript 170 | application/x-javascript 171 | application/json 172 | application/xml 173 | application/rss+xml 174 | image/svg+xml; 175 | 176 | access_log off; 177 | #access_log /var/log/nginx/nestjs.otaviomiranda.com.br-access.log; 178 | error_log /var/log/nginx/nestjs.otaviomiranda.com.br-error.log; 179 | 180 | #include /etc/nginx/common/protect.conf; 181 | } 182 | 183 | 184 | sudo ln /etc/nginx/sites-available/nestjs.otaviomiranda.com.br /etc/nginx/sites-enabled/nestjs.otaviomiranda.com.br 185 | sudo systemctl restart nginx 186 | 187 | 188 | 189 | # PM2 190 | npm i -g pm2 191 | pm2 start server.js --name nestjs 192 | pm2 startup 193 | pm2 save 194 | 195 | 196 | # NGINX UPLOAD 197 | sudo nano /etc/nginx/nginx.conf 198 | 199 | client_max_body_size 5M; 200 | 201 | sudo systemctl restart nginx 202 | ``` 203 | -------------------------------------------------------------------------------- /test/pessoas.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, HttpStatus, ValidationPipe } from '@nestjs/common'; 2 | import * as request from 'supertest'; 3 | import { ConfigModule } from '@nestjs/config'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import globalConfig from 'src/global-config/global.config'; 6 | import { ServeStaticModule } from '@nestjs/serve-static'; 7 | import { RecadosModule } from 'src/recados/recados.module'; 8 | import { PessoasModule } from 'src/pessoas/pessoas.module'; 9 | import { GlobalConfigModule } from 'src/global-config/global-config.module'; 10 | import * as path from 'path'; 11 | import { AppModule } from 'src/app/app.module'; 12 | import { ParseIntIdPipe } from 'src/common/pipes/parse-int-id.pipe'; 13 | import { Test, TestingModule } from '@nestjs/testing'; 14 | import { CreatePessoaDto } from 'src/pessoas/dto/create-pessoa.dto'; 15 | 16 | const login = async ( 17 | app: INestApplication, 18 | email: string, 19 | password: string, 20 | ) => { 21 | const response = await request(app.getHttpServer()) 22 | .post('/auth') 23 | .send({ email, password }); 24 | 25 | return response.body.accessToken; 26 | }; 27 | 28 | const createUserAndLogin = async (app: INestApplication) => { 29 | const nome = 'Any User'; 30 | const email = 'anyuser@email.com'; 31 | const password = '123456'; 32 | 33 | await request(app.getHttpServer()).post('/pessoas').send({ 34 | nome, 35 | email, 36 | password, 37 | }); 38 | 39 | return login(app, email, password); 40 | }; 41 | 42 | describe('PessoasController (e2e)', () => { 43 | let app: INestApplication; 44 | let authToken: string; 45 | 46 | beforeEach(async () => { 47 | const module: TestingModule = await Test.createTestingModule({ 48 | imports: [ 49 | ConfigModule.forFeature(globalConfig), 50 | TypeOrmModule.forRoot({ 51 | type: 'postgres', 52 | host: 'localhost', 53 | port: 5432, 54 | username: 'postgres', 55 | database: 'testing', // MUITO CUIDADO 56 | password: '123456', 57 | autoLoadEntities: true, 58 | synchronize: true, // MUITO CUIDADO 59 | dropSchema: true, // MUITO CUIDADO 60 | }), 61 | ServeStaticModule.forRoot({ 62 | rootPath: path.resolve(__dirname, '..', '..', 'pictures'), 63 | serveRoot: '/pictures', 64 | }), 65 | RecadosModule, 66 | PessoasModule, 67 | GlobalConfigModule, 68 | AppModule, 69 | ], 70 | }).compile(); 71 | 72 | app = module.createNestApplication(); 73 | 74 | app.useGlobalPipes( 75 | new ValidationPipe({ 76 | whitelist: true, 77 | forbidNonWhitelisted: true, 78 | transform: false, 79 | }), 80 | new ParseIntIdPipe(), 81 | ); 82 | await app.init(); 83 | 84 | authToken = await createUserAndLogin(app); 85 | }); 86 | 87 | afterEach(async () => { 88 | await app.close(); 89 | }); 90 | 91 | describe('POST /pessoas', () => { 92 | it('deve criar uma pessoa sem erros', async () => { 93 | const createPessoaDto: CreatePessoaDto = { 94 | email: 'luiz@email.com', 95 | nome: 'Luiz', 96 | password: '123456', 97 | }; 98 | const response = await request(app.getHttpServer()) 99 | .post('/pessoas') 100 | .send(createPessoaDto) 101 | .expect(HttpStatus.CREATED); 102 | 103 | expect(response.body).toEqual({ 104 | active: true, 105 | createdAt: expect.any(String), 106 | email: createPessoaDto.email, 107 | id: expect.any(Number), 108 | nome: createPessoaDto.nome, 109 | passwordHash: expect.any(String), 110 | picture: '', 111 | updatedAt: expect.any(String), 112 | }); 113 | }); 114 | 115 | it('deve gerar um erro de e-mail já existe', async () => { 116 | const createPessoaDto: CreatePessoaDto = { 117 | email: 'luiz@email.com', 118 | nome: 'Luiz', 119 | password: '123456', 120 | }; 121 | 122 | await request(app.getHttpServer()) 123 | .post('/pessoas') 124 | .send(createPessoaDto) 125 | .expect(HttpStatus.CREATED); 126 | 127 | const response = await request(app.getHttpServer()) 128 | .post('/pessoas') 129 | .send(createPessoaDto) 130 | .expect(HttpStatus.CONFLICT); 131 | 132 | expect(response.body.message).toBe('E-mail já está cadastrado.'); 133 | }); 134 | 135 | it('deve gerar um erro de senha curta', async () => { 136 | const createPessoaDto: CreatePessoaDto = { 137 | email: 'luiz@email.com', 138 | nome: 'Luiz', 139 | password: '123', 140 | }; 141 | 142 | const response = await request(app.getHttpServer()) 143 | .post('/pessoas') 144 | .send(createPessoaDto) 145 | .expect(HttpStatus.BAD_REQUEST); 146 | 147 | expect(response.body.message).toEqual([ 148 | 'password must be longer than or equal to 5 characters', 149 | ]); 150 | }); 151 | }); 152 | 153 | describe('GET /pessoas', () => { 154 | it('deve retornar todas as pessoas', async () => { 155 | await request(app.getHttpServer()) 156 | .post('/pessoas') 157 | .send({ 158 | email: 'luiz@email.com', 159 | nome: 'Luiz', 160 | password: '123456', 161 | }) 162 | .expect(HttpStatus.CREATED); 163 | 164 | const response = await request(app.getHttpServer()) 165 | .get('/pessoas') 166 | .set('Authorization', `Bearer ${authToken}`) 167 | .expect(HttpStatus.OK); 168 | 169 | expect(response.body).toEqual( 170 | expect.arrayContaining([ 171 | expect.objectContaining({ 172 | id: expect.any(Number), 173 | email: 'luiz@email.com', 174 | nome: 'Luiz', 175 | }), 176 | ]), 177 | ); 178 | }); 179 | }); 180 | 181 | describe('GET /pessoas/:id', () => { 182 | it('deve retornar uma pessoa pelo ID', async () => { 183 | const createResponse = await request(app.getHttpServer()) 184 | .post('/pessoas') 185 | .send({ 186 | email: 'luiz@email.com', 187 | nome: 'Luiz', 188 | password: '123456', 189 | }) 190 | .expect(HttpStatus.CREATED); 191 | 192 | const personId = createResponse.body.id; 193 | 194 | const response = await request(app.getHttpServer()) 195 | .get(`/pessoas/${personId}`) 196 | .set('Authorization', `Bearer ${authToken}`) 197 | .expect(HttpStatus.OK); 198 | 199 | expect(response.body).toEqual( 200 | expect.objectContaining({ 201 | id: personId, 202 | email: 'luiz@email.com', 203 | nome: 'Luiz', 204 | }), 205 | ); 206 | }); 207 | 208 | it('deve retornar erro para pessoa não encontrada', async () => { 209 | await request(app.getHttpServer()) 210 | .get('/pessoas/9999') // ID fictício 211 | .set('Authorization', `Bearer ${authToken}`) 212 | .expect(HttpStatus.NOT_FOUND); 213 | }); 214 | }); 215 | 216 | describe('PATCH /pessoas/:id', () => { 217 | it('deve atualizar uma pessoa', async () => { 218 | const createResponse = await request(app.getHttpServer()) 219 | .post('/pessoas') 220 | .send({ 221 | email: 'luiz@email.com', 222 | nome: 'Luiz', 223 | password: '123456', 224 | }) 225 | .expect(HttpStatus.CREATED); 226 | 227 | const personId = createResponse.body.id; 228 | 229 | const authToken = await login(app, 'luiz@email.com', '123456'); 230 | 231 | const updateResponse = await request(app.getHttpServer()) 232 | .patch(`/pessoas/${personId}`) 233 | .send({ 234 | nome: 'Luiz Atualizado', 235 | }) 236 | .set('Authorization', `Bearer ${authToken}`) 237 | .expect(HttpStatus.OK); 238 | 239 | expect(updateResponse.body).toEqual( 240 | expect.objectContaining({ 241 | id: personId, 242 | nome: 'Luiz Atualizado', 243 | }), 244 | ); 245 | }); 246 | 247 | it('deve retornar erro para pessoa não encontrada', async () => { 248 | await request(app.getHttpServer()) 249 | .patch('/pessoas/9999') // ID fictício 250 | .send({ 251 | nome: 'Nome Atualizado', 252 | }) 253 | .set('Authorization', `Bearer ${authToken}`) 254 | .expect(HttpStatus.NOT_FOUND); 255 | }); 256 | }); 257 | 258 | describe('DELETE /pessoas/:id', () => { 259 | it('deve remover uma pessoa', async () => { 260 | const createResponse = await request(app.getHttpServer()) 261 | .post('/pessoas') 262 | .send({ 263 | email: 'luiz@email.com', 264 | nome: 'Luiz', 265 | password: '123456', 266 | }) 267 | .expect(HttpStatus.CREATED); 268 | 269 | const authToken = await login(app, 'luiz@email.com', '123456'); 270 | 271 | const personId = createResponse.body.id; 272 | 273 | const response = await request(app.getHttpServer()) 274 | .delete(`/pessoas/${personId}`) 275 | .set('Authorization', `Bearer ${authToken}`) 276 | .expect(HttpStatus.OK); 277 | 278 | expect(response.body.email).toBe('luiz@email.com'); 279 | }); 280 | 281 | it('deve retornar erro para pessoa não encontrada', async () => { 282 | await request(app.getHttpServer()) 283 | .delete('/pessoas/9999') // ID fictício 284 | .set('Authorization', `Bearer ${authToken}`) 285 | .expect(HttpStatus.NOT_FOUND); 286 | }); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /AULAS.md: -------------------------------------------------------------------------------- 1 | 1. Introdução ao curso - 6m 2 | 2. Requisitos do NestJS e ambiente de desenvolvimento do curso - 9m 3 | 3. Instalando o @nestjs/cli - Interface de linha de comando do NestJS - 13m 4 | 4. Conheça e entenda os arquivos básicos gerados pelo NestJS CLI - 17m 5 | 5. Arquivo main.ts, função bootstrap(), NestFactory e porta do app NestJS - 10m 6 | 6. Criando meu próprio módulo (Module) manualmente ou com Nest CLI - 19m 7 | 7. Sobre o código das aulas no Github - 3m 8 | 8. Básico sobre protocolo HTTP, URLs, recursos e métodos de solicitação (GET) - 9 | 17m 10 | 9. Continuação da aula anterior + Extensão Rest Client do VS Code - 10m 11 | 10. Criando seu Primeiro Controller - Manualmente ou com NestJS CLI - 15m 12 | 11. Service / Provider - entendendo o básico da injeção de dependências do 13 | NestJS - 16m 14 | 12. Novo módulo (module) e controller (controlador) para nossa aplicação 15 | NestJS - 8m 16 | 13. Informação importante sobre nomenclatura (use inglês sempre que possível) - 17 | 1m 18 | 14. Criando parâmetros de rota de forma dinâmica para o ID do recurso - 19 | @Get(':id') - 6m 20 | 15. O decorator @Param é usado para ler valores dos parâmetros de rotas - 6m 21 | 16. Método HTTP POST e decorator @Post - Uma rota para criar coisas no NestJS - 22 | 5m 23 | 17. @Body - Usado para ler valores do corpo da requisição HTTP POST - 10m 24 | 18. Decorator HttpCode e Enum HttpStatus para códigos de status de respostas 25 | HTTP - 9m 26 | 19. Rota para atualizar um recado (update) com métodos PATCH ou PUT e 27 | decoradores - 8m 28 | 20. Rota para apagar um recado (delete) com método DELETE e decorador Delete - 29 | 3m 30 | 21. Query parameters (parâmetros de consulta) da URL para exemplo de paginação - 31 | 7m 32 | 22. Configurando o VS Code, tema, Eslint e Prettier para manter um código 33 | uniforme - 12m 34 | 23. Criando o provider (service) RecadosService - 6m 35 | 24. Criando uma Entity (Entidade) Recado e concluindo nosso service de BD em 36 | memória - 22m 37 | 25. Usando HttpException e NotFoundException para exibir mensagens de erro 38 | HTTP - 14m 39 | 26. DTOs (Data Transfer Object) para transportar, validar e transformar dados - 40 | 15m 41 | 27. Validando dados de entrada com useGlobalPipes, ValidationPipe e 42 | class-validator - 13m 43 | 28. Use PartialType de Mapped-Types para a validação de campos em 44 | UpdateRecadoDto - 10m 45 | 29. Segurança - opções whitelist e forbidNonWhitelisted de ValidationPipe do 46 | NestJS - 5m 47 | 30. Converter tipos c/ transform do ValidationPipe e Pipes de Param padrão no 48 | NestJS - 16m 49 | 31. Instalando o Banco de Dados PostgreSQL 16 e o DBeaver CE no Windows - 15m 50 | 32. Configurando o TypeOrmModule com PostgreSQL (módulo do TypeORM para 51 | NestJS) - 8m 52 | 33. Primeira Entity do TypeOrm no NestJS (primeira tabela na base de dados 53 | PSQL) - 13m 54 | 34. Usando InjectRepository e Repository para ler e manipular a entity na 55 | tabela - 11m 56 | 35. Usando o Repository para criar (create) e apagar (delete) uma Entity da 57 | tabela - 8m 58 | 36. Usando o Repository para atualizar (update) uma Entity na tabela - 7m 59 | 37. Criando um módulo (Resource) para Entity "Pessoa" (usuário da nossa 60 | aplicação) - 12m 61 | 38. CRUD de Pessoa: criando o create com e-mail único na tabela (unique) - 16m 62 | 39. CRUD de Pessoa: criando o findAll, findOne (Read) e remove (Delete) - 5m 63 | 40. CRUD de Pessoa: criando o Update - 6m 64 | 41. Relações ManyToOne e OneToMany entre Entities Recado e Pessoa - 13m 65 | 42. Injetando dependências de outros módulos c/ "imports" e "exports" de 66 | Module - 7m 67 | 43. Criando um Recado com relação ManyToOne com Pessoa - 11m 68 | 44. Usando onDelete e onUpdate CASCADE para propagar alterações com 69 | relacionamentos - 5m 70 | 45. Modificando o Updade (Atualização) do Service de Recado para retornar a 71 | relação - 5m 72 | 46. DTO e validação de dados para paginação (take/limit e skip/offset do 73 | TypeOrm) - 20m Iniciar 74 | 47. Documentação Oficial do TypeORM e NestJS/TypeORM - 1m 75 | 48. Pipes - como e quando usar um Pipe de Validação e/ou Transformação de 76 | dados - 18m 77 | 49. Interceptors - Adicionando cabeçalho à resposta HTTP com NestJS 78 | Interceptor - 13m 79 | 50. Interceptors - observando dados antes e depois da execução do método com 80 | tap - 9m 81 | 51. Interceptors - capturando e modificando erros globais da aplicação 82 | (Exceptions) - 9m 83 | 52. Interceptors - criando um cache simples - 10m 84 | 53. Interceptors - alterando os dados de resposta com map - 6m 85 | 54. Injeção de dependência em Interceptors, Pipes e outras classes 86 | (Injectable) - 7m 87 | 55. Usando Interceptors para autorização de token de login (Authorization 88 | Token) - 10m 89 | 56. Middleware - Tenha acesso direto à Request e Response do Servidor - 27m 90 | 57. Exception Filters - Filtrando e manipulando exceções no NestJS - 23m 91 | 58. Guards - Como permitir ou negar acesso em rotas do servidor NestJs - 14m 92 | 59. Limpeza do código - 5m 93 | 60. Parâmetros personalizados com createParamDecorator (Custom Param 94 | Decorator) - 9m 95 | 61. O básico da injeção de dependências no NestJS (Dependency Injection - DI) - 96 | 7m 97 | 62. Encapsulamento e exports do módulo + Dependência circular e forwardRef no 98 | NestJS - 8m 99 | 63. Providers com useClass e useValue para entregar tokens e valores 100 | diferentes - 8m 101 | 64. Injetando valores que não são classes com "Inject" e "provide" (token) - 6m 102 | 65. Classes abstratas e interfaces com provide e useClass para Padrões de 103 | Projeto - 19m 104 | 66. Provide, useFactory e inject para Lógica Avançada na Injeção de 105 | Dependências - 14m 106 | 67. Utilizando useFactory com async e await para Gerenciar Providers 107 | Assíncronos - 4m 108 | 68. Criando módulos dinâmicos que recebem configuração (NestJS DynamicModule) - 109 | 16m 110 | 69. Teoria sobre escopo de providers: Scope.DEFAULT, Scope.REQUEST e 111 | Scope.TRANSIENT - 8m 112 | 70. Exemplo para escopo de providers: Scope.DEFAULT, Scope.REQUEST e 113 | Scope.TRANSIENT - 11m 114 | 71. Instalando @nestjs/config e criando variáveis de ambiente (.env) no NestJS - 115 | 16m 116 | 72. Quando, qual e de onde carregar o arquivo .env? Use envFilePath e 117 | ignoreEnvFile - 4m 118 | 73. Usando @hapi/joi para validar as configurações do .env (variáveis de 119 | ambiente) - 7m 120 | 74. Usando o ConfigService do ConfigModule para obter valores de dentro do 121 | .env - 6m 122 | 75. Usando o ConfigService para obter valores do .env diretamente no módulo - 123 | 12m 124 | 76. Criando configurações parciais com namespaces, registerAs, ConfigType e 125 | mais - 13m 126 | 77. Devo separar as configurações em um módulo à parte? Depende! - 6m 127 | 78. O que é JWT e como funcionará nosso sistema de autenticação e autorização - 128 | 14m 129 | 79. Criando o sistema de Hashing com Bcrypt e o AuthModule (módulo de 130 | autenticação) - 11m 131 | 80. Salvando o hash de senha do bcrypt na base de dados ao criar um usuário - 8m 132 | 81. Criando o AuthService e AuthController para rota de Login do usuário - 9m 133 | 82. Autenticando o usuário com email e senha usando o Hash de senha e 134 | HashingService - 13m 135 | 83. Namespace das configurações e .env que serão usados para o JwtModule - 8m 136 | 84. Instalando o @nestjs/jwt e assinando o jwt token com JwtService e 137 | JwtModule - 9m 138 | 85. AuthTokenGuard será o Guard usado para permitir ou bloquear acesso às 139 | rotas - 17m 140 | 86. Dica: usando variáveis para pegar o response.body.accessToken no Rest 141 | Client - 4m 142 | 87. Enviando dados do token payload usando TokenPayloadDto para controller e 143 | service - 8m 144 | 88. Modificando update e remove em Pessoa para usar os dados do Jwt 145 | TokenPayload - 9m 146 | 89. Modificando create, update e remove de Recado com dados do Jwt 147 | TokenPayload - 13m 148 | 90. Como funcionam os Refresh Token em JWT? (Aula extra) - 7m 149 | 91. Gerando o Refresh Token junto com o Access Token (Aula extra) - 14m 150 | 92. Gerando novos tokens usando o Refresh Token (Aula extra) - 10m 151 | 93. Tem como invalidar tokens ou bloquear o usuário? Entenda e resolva! - 11m 152 | 94. Entendendo Reflector e SetMetadata para metadados (Policy-Based 153 | Authorization) - 19m 154 | 95. Campo Enum para Route Policies em Pessoa Entity (Policy-Based 155 | Authorization) - 14m 156 | 96. Concluindo o RoutePolicyGuard (Policy-Based Authorization) - 14m 157 | 97. Limpeza do código (Removendo Policy-Based Authorization) - 5m 158 | 98. Rest Client com multipart/form-data e FileInterceptor + UploadedFile - 19m 159 | 99. Salvando o Buffer do arquivo enviado no disco do servidor - 9m 160 | 100. FilesInterceptor e UploadedFiles para upload de múltiplos arquivos - 6m 161 | 101. Validação de arquivos enviados com ParseFilePipeBuilder e ParseFilePipe - 162 | 12m 163 | 102. Usando ServeStaticModule para servir arquivos estáticos no NestJS - 6m 164 | 103. Adicionando o campo picture na base de dados e movendo a lógica para o 165 | service - 8m 166 | 104. Alerta: testes podem ser complexos - 9m 167 | 105. Scripts do package.json para executar testes com Jest no NestJS - 5m 168 | 106. Escrevendo meu primeiro teste com Jest no NestJs - 169 | pessoas.service.spec.ts - 19m 170 | 107. Usando TestingModule e Test de nestjs/testing para configurar todo o nosso 171 | teste - 12m 172 | 108. Definindo o que você vai testar - 8m 173 | 109. Usando jest.fn e jest.spyOn para simular chamadas e retornos de métodos - 174 | 13m 175 | 110. Completando o teste e entendendo linha a linha o que estamos testando - 9m 176 | 111. Escrevendo um teste para garantir que uma Exception ocorreu - 7m 177 | 112. Use os relatórios de coverage para conferir se tudo está testado 178 | corretamente - 9m 179 | 113. Teste unitário do método findOne do PessoasService e verificando coverage - 180 | 11m 181 | 114. Teste unitário do método findAll em PessoasService - 6m 182 | 115. Teste unitário - método update de PessoasService (caso onde Pessoa é 183 | atualizada) - 11m 184 | 116. Teste do método update em PessoasService (NotFoundException, 185 | ForbiddenException) - 4m 186 | 117. Teste unitário do método remove em PessoasService (Completo) - 4m 187 | 118. Testes unitários finais do PessoasService (uploadPicture) + coverage 100% - 188 | 8m 189 | 119. Testes unitários de PessoasController sem usar Test e TestingModule - 8m 190 | 120. Testes unitários para validação dos DTOs - 4m 191 | 121. Configurando os testes end-to-end (E2E) - 18m 192 | 122. Teste E2E (end-to-end): deve criar uma pessoa com sucesso - /pessoas 193 | (POST) - 12m 194 | 123. Exemplos de testes E2E (end-to-end) para te ajudar a criar seus próprios 195 | testes - 6m 196 | 124. Teste E2E com JWT Authorization Token - 16m 197 | 125. Mais exemplos de testes E2E em código para a rota /pessoas - 2m 198 | 126. Segurança e BUILD: .env, .env-example, CORS, Helmet, ThrottlerModule e 199 | mais - 20m 200 | 127. Masterclass Deploy - Criando um servidor Ubuntu na Google Cloud Platform 201 | (GCP) - 31m 202 | 128. Masterclass Deploy - Configurando PostgreSQL, Node.js, Letsencrypt, Nginx e 203 | PM2 - 38m 204 | 129. Masterclass Deploy - modificações na sua API REST (Local -> Git -> 205 | Remoto)? - 13m 206 | -------------------------------------------------------------------------------- /src/pessoas/pessoas.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from 'typeorm'; 2 | import { PessoasService } from './pessoas.service'; 3 | import { Pessoa } from './entities/pessoa.entity'; 4 | import { HashingService } from 'src/auth/hashing/hashing.service'; 5 | import { Test, TestingModule } from '@nestjs/testing'; 6 | import { getRepositoryToken } from '@nestjs/typeorm'; 7 | import { CreatePessoaDto } from './dto/create-pessoa.dto'; 8 | import { 9 | BadRequestException, 10 | ConflictException, 11 | ForbiddenException, 12 | NotFoundException, 13 | } from '@nestjs/common'; 14 | import * as path from 'path'; 15 | import * as fs from 'fs/promises'; 16 | 17 | jest.mock('fs/promises'); // Mocka o módulo fs 18 | 19 | describe('PessoasService', () => { 20 | let pessoasService: PessoasService; 21 | let pessoaRepository: Repository; 22 | let hashingService: HashingService; 23 | 24 | beforeEach(async () => { 25 | const module: TestingModule = await Test.createTestingModule({ 26 | providers: [ 27 | PessoasService, 28 | { 29 | provide: getRepositoryToken(Pessoa), 30 | useValue: { 31 | save: jest.fn(), 32 | create: jest.fn(), 33 | findOneBy: jest.fn(), 34 | find: jest.fn(), 35 | preload: jest.fn(), 36 | remove: jest.fn(), 37 | }, 38 | }, 39 | { 40 | provide: HashingService, 41 | useValue: { 42 | hash: jest.fn(), 43 | }, 44 | }, 45 | ], 46 | }).compile(); 47 | 48 | pessoasService = module.get(PessoasService); 49 | pessoaRepository = module.get>( 50 | getRepositoryToken(Pessoa), 51 | ); 52 | hashingService = module.get(HashingService); 53 | }); 54 | 55 | it('pessoaService deve estar definido', () => { 56 | expect(pessoasService).toBeDefined(); 57 | }); 58 | 59 | describe('create', () => { 60 | it('deve criar uma nova pessoa', async () => { 61 | // Arange 62 | const createPessoaDto: CreatePessoaDto = { 63 | email: 'luiz@email.com', 64 | nome: 'Luiz', 65 | password: '123456', 66 | }; 67 | const passwordHash = 'HASHDESENHA'; 68 | const novaPessoa = { 69 | id: 1, 70 | nome: createPessoaDto.nome, 71 | email: createPessoaDto.email, 72 | passwordHash, 73 | }; 74 | 75 | // Como o valor retornado por hashingService.hash é necessário 76 | // vamos simular este valor. 77 | jest.spyOn(hashingService, 'hash').mockResolvedValue(passwordHash); 78 | // Como a pessoa retornada por pessoaRepository.create é necessária em 79 | // pessoaRepository.save. Vamos simular este valor. 80 | jest.spyOn(pessoaRepository, 'create').mockReturnValue(novaPessoa as any); 81 | 82 | // Act -> Ação 83 | const result = await pessoasService.create(createPessoaDto); 84 | 85 | // Assert 86 | // O método hashingService.hash foi chamado com createPessoaDto.password? 87 | expect(hashingService.hash).toHaveBeenCalledWith( 88 | createPessoaDto.password, 89 | ); 90 | 91 | // O método pessoaRepository.create foi chamado com os dados da nova 92 | // pessoa com o hash de senha gerado por hashingService.hash? 93 | expect(pessoaRepository.create).toHaveBeenCalledWith({ 94 | nome: createPessoaDto.nome, 95 | passwordHash, 96 | email: createPessoaDto.email, 97 | }); 98 | 99 | // O método pessoaRepository.save foi chamado com os dados da nova 100 | // pessoa gerada por pessoaRepository.create? 101 | expect(pessoaRepository.save).toHaveBeenCalledWith(novaPessoa); 102 | 103 | // O resultado do método pessoaService.create retornou a nova 104 | // pessoa criada? 105 | expect(result).toEqual(novaPessoa); 106 | }); 107 | 108 | it('deve lançar ConflictException quando e-mail já existe', async () => { 109 | jest.spyOn(pessoaRepository, 'save').mockRejectedValue({ 110 | code: '23505', 111 | }); 112 | 113 | await expect(pessoasService.create({} as any)).rejects.toThrow( 114 | ConflictException, 115 | ); 116 | }); 117 | 118 | it('deve lançar um erro genérico quando um erro for lançado', async () => { 119 | jest 120 | .spyOn(pessoaRepository, 'save') 121 | .mockRejectedValue(new Error('Erro genérico')); 122 | 123 | await expect(pessoasService.create({} as any)).rejects.toThrow( 124 | new Error('Erro genérico'), 125 | ); 126 | }); 127 | }); 128 | 129 | describe('findOne', () => { 130 | it('deve retornar uma pessoa se a pessoa for encontrada', async () => { 131 | const pessoaId = 1; 132 | const pessoaEncontrada = { 133 | id: pessoaId, 134 | nome: 'Luiz', 135 | email: 'luiz@email.com', 136 | passwordHash: '123456', 137 | }; 138 | 139 | jest 140 | .spyOn(pessoaRepository, 'findOneBy') 141 | .mockResolvedValue(pessoaEncontrada as any); 142 | 143 | const result = await pessoasService.findOne(pessoaId); 144 | 145 | expect(result).toEqual(pessoaEncontrada); 146 | }); 147 | 148 | it('deve lançar um erro se a pessoa não for encontrada', async () => { 149 | await expect(pessoasService.findOne(1)).rejects.toThrow( 150 | NotFoundException, 151 | ); 152 | }); 153 | }); 154 | 155 | describe('findAll', () => { 156 | it('deve retornar todas as pessoas', async () => { 157 | const pessoasMock: Pessoa[] = [ 158 | { 159 | id: 1, 160 | nome: 'Luiz', 161 | email: 'luiz@email.com', 162 | passwordHash: '123456', 163 | } as Pessoa, 164 | ]; 165 | 166 | jest.spyOn(pessoaRepository, 'find').mockResolvedValue(pessoasMock); 167 | 168 | const result = await pessoasService.findAll(); 169 | 170 | expect(result).toEqual(pessoasMock); 171 | expect(pessoaRepository.find).toHaveBeenCalledWith({ 172 | order: { 173 | id: 'desc', 174 | }, 175 | }); 176 | }); 177 | }); 178 | 179 | describe('update', () => { 180 | it('deve atualizar uma pessoa se for autorizado', async () => { 181 | // Arrange 182 | const pessoaId = 1; 183 | const updatePessoaDto = { nome: 'Joana', password: '654321' }; 184 | const tokenPayload = { sub: pessoaId } as any; 185 | const passwordHash = 'HASHDESENHA'; 186 | const updatedPessoa = { id: pessoaId, nome: 'Joana', passwordHash }; 187 | 188 | jest.spyOn(hashingService, 'hash').mockResolvedValueOnce(passwordHash); 189 | jest 190 | .spyOn(pessoaRepository, 'preload') 191 | .mockResolvedValue(updatedPessoa as any); 192 | jest 193 | .spyOn(pessoaRepository, 'save') 194 | .mockResolvedValue(updatedPessoa as any); 195 | 196 | // Act 197 | const result = await pessoasService.update( 198 | pessoaId, 199 | updatePessoaDto, 200 | tokenPayload, 201 | ); 202 | 203 | // Assert 204 | expect(hashingService.hash).toHaveBeenCalledWith( 205 | updatePessoaDto.password, 206 | ); 207 | expect(pessoaRepository.preload).toHaveBeenCalledWith({ 208 | id: pessoaId, 209 | nome: updatePessoaDto.nome, 210 | passwordHash, 211 | }); 212 | expect(pessoaRepository.save).toHaveBeenCalledWith(updatedPessoa); 213 | expect(result).toEqual(updatedPessoa); 214 | }); 215 | 216 | it('deve lançar ForbiddenException se usuário não autorizado', async () => { 217 | // Arrange 218 | const pessoaId = 1; // Usuário certo (ID 1) 219 | const tokenPayload = { sub: 2 } as any; // Usuário diferente (ID 2) 220 | const updatePessoaDto = { nome: 'Jane Doe' }; 221 | const existingPessoa = { id: pessoaId, nome: 'John Doe' }; 222 | 223 | // Simula que a pessoa existe 224 | jest 225 | .spyOn(pessoaRepository, 'preload') 226 | .mockResolvedValue(existingPessoa as any); 227 | 228 | // Act e Assert 229 | await expect( 230 | pessoasService.update(pessoaId, updatePessoaDto, tokenPayload), 231 | ).rejects.toThrow(ForbiddenException); 232 | }); 233 | 234 | it('deve lançar NotFoundException se a pessoa não existe', async () => { 235 | // Arrange 236 | const pessoaId = 1; 237 | const tokenPayload = { sub: pessoaId } as any; 238 | const updatePessoaDto = { nome: 'Jane Doe' }; 239 | 240 | // Simula que preload retornou null 241 | jest.spyOn(pessoaRepository, 'preload').mockResolvedValue(null); 242 | 243 | // Act e Assert 244 | await expect( 245 | pessoasService.update(pessoaId, updatePessoaDto, tokenPayload), 246 | ).rejects.toThrow(NotFoundException); 247 | }); 248 | }); 249 | 250 | describe('remove', () => { 251 | it('deve remover uma pessoa se autorizado', async () => { 252 | // Arrange 253 | const pessoaId = 1; // Pessoa com ID 1 254 | const tokenPayload = { sub: pessoaId } as any; // Usuário com ID 1 255 | const existingPessoa = { id: pessoaId, nome: 'John Doe' }; // Pessoa é o Usuário 256 | 257 | // findOne do service vai retornar a pessoa existente 258 | jest 259 | .spyOn(pessoasService, 'findOne') 260 | .mockResolvedValue(existingPessoa as any); 261 | // O método remove do repositório também vai retornar a pessoa existente 262 | jest 263 | .spyOn(pessoaRepository, 'remove') 264 | .mockResolvedValue(existingPessoa as any); 265 | 266 | // Act 267 | const result = await pessoasService.remove(pessoaId, tokenPayload); 268 | 269 | // Assert 270 | // Espero que findOne do pessoaService seja chamado com o ID da pessoa 271 | expect(pessoasService.findOne).toHaveBeenCalledWith(pessoaId); 272 | // Espero que o remove do repositório seja chamado com a pessoa existente 273 | expect(pessoaRepository.remove).toHaveBeenCalledWith(existingPessoa); 274 | // Espero que a pessoa apagada seja retornada 275 | expect(result).toEqual(existingPessoa); 276 | }); 277 | 278 | it('deve lançar ForbiddenException se não autorizado', async () => { 279 | // Arrange 280 | const pessoaId = 1; // Pessoa com ID 1 281 | const tokenPayload = { sub: 2 } as any; // Usuário com ID 2 282 | const existingPessoa = { id: pessoaId, nome: 'John Doe' }; // Pessoa NÃO é o Usuário 283 | 284 | // Espero que o findOne seja chamado com pessoa existente 285 | jest 286 | .spyOn(pessoasService, 'findOne') 287 | .mockResolvedValue(existingPessoa as any); 288 | 289 | // Espero que o servico rejeite porque o usuário é diferente da pessoa 290 | await expect( 291 | pessoasService.remove(pessoaId, tokenPayload), 292 | ).rejects.toThrow(ForbiddenException); 293 | }); 294 | 295 | it('deve lançar NotFoundException se a pessoa não for encontrada', async () => { 296 | const pessoaId = 1; 297 | const tokenPayload = { sub: pessoaId } as any; 298 | 299 | // Só precisamos que o findOne lance uma exception e o remove também deve lançar 300 | jest 301 | .spyOn(pessoasService, 'findOne') 302 | .mockRejectedValue(new NotFoundException()); 303 | 304 | await expect( 305 | pessoasService.remove(pessoaId, tokenPayload), 306 | ).rejects.toThrow(NotFoundException); 307 | }); 308 | }); 309 | 310 | describe('uploadPicture', () => { 311 | it('deve salvar a imagem corretamente e atualizar a pessoa', async () => { 312 | // Arrange 313 | const mockFile = { 314 | originalname: 'test.png', 315 | size: 2000, 316 | buffer: Buffer.from('file content'), 317 | } as Express.Multer.File; 318 | 319 | const mockPessoa = { 320 | id: 1, 321 | nome: 'Luiz', 322 | email: 'luiz@email.com', 323 | } as Pessoa; 324 | 325 | const tokenPayload = { sub: 1 } as any; 326 | 327 | jest.spyOn(pessoasService, 'findOne').mockResolvedValue(mockPessoa); 328 | jest.spyOn(pessoaRepository, 'save').mockResolvedValue({ 329 | ...mockPessoa, 330 | picture: '1.png', 331 | }); 332 | 333 | const filePath = path.resolve(process.cwd(), 'pictures', '1.png'); 334 | 335 | // Act 336 | const result = await pessoasService.uploadPicture(mockFile, tokenPayload); 337 | 338 | // Assert 339 | expect(fs.writeFile).toHaveBeenCalledWith(filePath, mockFile.buffer); 340 | expect(pessoaRepository.save).toHaveBeenCalledWith({ 341 | ...mockPessoa, 342 | picture: '1.png', 343 | }); 344 | expect(result).toEqual({ 345 | ...mockPessoa, 346 | picture: '1.png', 347 | }); 348 | }); 349 | 350 | it('deve lançar BadRequestException se o arquivo for muito pequeno', async () => { 351 | // Arrange 352 | const mockFile = { 353 | originalname: 'test.png', 354 | size: 500, // Menor que 1024 bytes 355 | buffer: Buffer.from('small content'), 356 | } as Express.Multer.File; 357 | 358 | const tokenPayload = { sub: 1 } as any; 359 | 360 | // Act & Assert 361 | await expect( 362 | pessoasService.uploadPicture(mockFile, tokenPayload), 363 | ).rejects.toThrow(BadRequestException); 364 | }); 365 | 366 | it('deve lançar NotFoundException se a pessoa não for encontrada', async () => { 367 | // Arrange 368 | const mockFile = { 369 | originalname: 'test.png', 370 | size: 2000, 371 | buffer: Buffer.from('file content'), 372 | } as Express.Multer.File; 373 | 374 | const tokenPayload = { sub: 1 } as any; 375 | 376 | jest 377 | .spyOn(pessoasService, 'findOne') 378 | .mockRejectedValue(new NotFoundException()); 379 | 380 | // Act & Assert 381 | await expect( 382 | pessoasService.uploadPicture(mockFile, tokenPayload), 383 | ).rejects.toThrow(NotFoundException); 384 | }); 385 | }); 386 | }); 387 | --------------------------------------------------------------------------------