├── .gitignore ├── .nvmrc ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── bcrypt.js ├── database └── migrations │ └── 1648623447697-addUser.ts ├── env ├── .env └── local.env ├── jest-e2e.json ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── app.module.ts ├── domain │ ├── adapters │ │ ├── bcrypt.interface.ts │ │ └── jwt.interface.ts │ ├── config │ │ ├── database.interface.ts │ │ └── jwt.interface.ts │ ├── exceptions │ │ └── exceptions.interface.ts │ ├── logger │ │ └── logger.interface.ts │ ├── model │ │ ├── auth.ts │ │ ├── todo.ts │ │ └── user.ts │ └── repositories │ │ ├── todoRepository.interface.ts │ │ └── userRepository.interface.ts ├── infrastructure │ ├── common │ │ ├── filter │ │ │ └── exception.filter.ts │ │ ├── guards │ │ │ ├── jwtAuth.guard.ts │ │ │ ├── jwtRefresh.guard.ts │ │ │ └── login.guard.ts │ │ ├── interceptors │ │ │ ├── logger.interceptor.ts │ │ │ └── response.interceptor.ts │ │ ├── strategies │ │ │ ├── jwt.strategy.ts │ │ │ ├── jwtRefresh.strategy.ts │ │ │ └── local.strategy.ts │ │ └── swagger │ │ │ └── response.decorator.ts │ ├── config │ │ ├── environment-config │ │ │ ├── environment-config.module.ts │ │ │ ├── environment-config.service.ts │ │ │ └── environment-config.validation.ts │ │ └── typeorm │ │ │ ├── typeorm.config.ts │ │ │ └── typeorm.module.ts │ ├── controllers │ │ ├── auth │ │ │ ├── auth-dto.class.ts │ │ │ ├── auth.controller.ts │ │ │ └── auth.presenter.ts │ │ ├── controllers.module.ts │ │ └── todo │ │ │ ├── todo.controller.ts │ │ │ ├── todo.dto.ts │ │ │ └── todo.presenter.ts │ ├── entities │ │ ├── todo.entity.ts │ │ └── user.entity.ts │ ├── exceptions │ │ ├── exceptions.module.ts │ │ └── exceptions.service.ts │ ├── logger │ │ ├── logger.module.ts │ │ └── logger.service.ts │ ├── repositories │ │ ├── repositories.module.ts │ │ ├── todo.repository.ts │ │ └── user.repository.ts │ ├── services │ │ ├── bcrypt │ │ │ ├── bcrypt.module.ts │ │ │ ├── bcrypt.service.spec.ts │ │ │ └── bcrypt.service.ts │ │ └── jwt │ │ │ ├── jwt.module.ts │ │ │ └── jwt.service.ts │ └── usecases-proxy │ │ ├── usecases-proxy.module.ts │ │ └── usecases-proxy.ts ├── main.ts └── usecases │ ├── auth │ ├── isAuthenticated.usecases.ts │ ├── login.usecases.ts │ ├── logout.usecases.ts │ └── test │ │ └── auth.spec.ts │ └── todo │ ├── addTodo.usecases.ts │ ├── deleteTodo.usecases.ts │ ├── getTodo.usecases.ts │ ├── getTodos.usecases.ts │ └── updateTodo.usecases.ts ├── test └── app.e2e-spec.ts ├── tsconfig.build.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | coverage-e2e 5 | test/coverage -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/iron -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 130 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } -------------------------------------------------------------------------------- /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 | ## Postgres 36 | 37 | Install docker and run the command: 38 | 39 | ```bash 40 | docker run --name clean-architecture-db -e POSTGRES_PASSWORD=docker -p 5432:5432 -d postgres 41 | ``` 42 | 43 | run migration 44 | 45 | ```bash 46 | npm run typeorm:generate:win -n init 47 | npm run typeorm:run:win 48 | ``` 49 | 50 | ## Running the app 51 | 52 | ```bash 53 | # development 54 | $ npm run start 55 | 56 | # watch mode 57 | $ npm run start:dev 58 | 59 | # production mode 60 | $ npm run start:prod 61 | ``` 62 | 63 | ## Test 64 | 65 | ```bash 66 | # unit tests 67 | $ npm run test 68 | 69 | # e2e tests 70 | $ npm run test:e2e 71 | 72 | # test coverage 73 | $ npm run test:cov 74 | ``` 75 | 76 | ## Support 77 | 78 | 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). 79 | 80 | ## Stay in touch 81 | 82 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 83 | - Website - [https://nestjs.com](https://nestjs.com/) 84 | - Twitter - [@nestframework](https://twitter.com/nestframework) 85 | 86 | ## License 87 | 88 | Nest is [MIT licensed](LICENSE). 89 | -------------------------------------------------------------------------------- /bcrypt.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | 3 | const password = 'jonathan'; 4 | const saltRounds = 10; 5 | 6 | // Hash the password 7 | bcrypt.hash(password, saltRounds, function (err, hash) { 8 | if (err) throw err; 9 | console.log('Hashed Password:', hash); 10 | }); 11 | -------------------------------------------------------------------------------- /database/migrations/1648623447697-addUser.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class addUser1648623447697 implements MigrationInterface { 4 | name = 'addUser1648623447697'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `CREATE TABLE "public"."todo" ("id" SERIAL NOT NULL, "content" character varying(255), "is_done" boolean NOT NULL DEFAULT false, "createdate" TIMESTAMP NOT NULL DEFAULT now(), "updateddate" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_e87731bafd682f5a84e2449470f" PRIMARY KEY ("id"))`, 9 | ); 10 | await queryRunner.query( 11 | `CREATE TABLE "public"."user" ("id" SERIAL NOT NULL, "username" character varying NOT NULL, "password" text NOT NULL, "createdate" TIMESTAMP NOT NULL DEFAULT now(), "updateddate" TIMESTAMP NOT NULL DEFAULT now(), "last_login" TIMESTAMP, "hach_refresh_token" character varying, CONSTRAINT "UQ_b67337b7f8aa8406e936c2ff754" UNIQUE ("username"), CONSTRAINT "PK_03b91d2b8321aa7ba32257dc321" PRIMARY KEY ("id"))`, 12 | ); 13 | await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b67337b7f8aa8406e936c2ff75" ON "public"."user" ("username") `); 14 | } 15 | 16 | public async down(queryRunner: QueryRunner): Promise { 17 | await queryRunner.query(`DROP INDEX "public"."IDX_b67337b7f8aa8406e936c2ff75"`); 18 | await queryRunner.query(`DROP TABLE "public"."user"`); 19 | await queryRunner.query(`DROP TABLE "public"."todo"`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /env/.env: -------------------------------------------------------------------------------- 1 | DATABASE_HOST=localhost 2 | DATABASE_PORT=5432 3 | DATABASE_USER=postgres 4 | DATABASE_PASSWORD=docker 5 | DATABASE_NAME=postgres 6 | DATABASE_SCHEMA=public 7 | DATABASE_SYNCHRONIZE=false 8 | JWT_SECRET=74YLbq4%c!wU 9 | JWT_EXPIRATION_TIME=1800 10 | JWT_REFRESH_TOKEN_SECRET=7jML9q4-c!s0 11 | JWT_REFRESH_TOKEN_EXPIRATION_TIME=86400 -------------------------------------------------------------------------------- /env/local.env: -------------------------------------------------------------------------------- 1 | DATABASE_HOST=localhost 2 | DATABASE_PORT=5432 3 | DATABASE_USER=postgres 4 | DATABASE_PASSWORD=docker 5 | DATABASE_NAME=postgres 6 | DATABASE_SCHEMA=public 7 | DATABASE_SYNCHRONIZE=false 8 | JWT_SECRET=74YLbq4%c!wU 9 | JWT_EXPIRATION_TIME=1800 10 | JWT_REFRESH_TOKEN_SECRET=7jML9q4-c!s0 11 | JWT_REFRESH_TOKEN_EXPIRATION_TIME=86400 -------------------------------------------------------------------------------- /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 | "coverageDirectory": "./coverage-e2e" 10 | } 11 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "engine": { 8 | "node": "20" 9 | }, 10 | "license": "UNLICENSED", 11 | "scripts": { 12 | "prebuild": "rimraf dist", 13 | "build": "nest build", 14 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 15 | "start": "nest start", 16 | "start:dev": "NODE_ENV=local nest start --watch", 17 | "start:debug": "nest start --debug --watch", 18 | "start:prod": "node dist/main", 19 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 20 | "test": "jest", 21 | "test:watch": "jest --watch", 22 | "test:cov": "jest --coverage", 23 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 24 | "test:e2e": "jest --config ./jest-e2e.json --coverage", 25 | "typeorm:win": "NODE_ENV=local ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --config src/infrastructure/config/typeorm/typeorm.config.ts", 26 | "typeorm:generate:win": "npm run typeorm:win migration:generate -- -n", 27 | "typeorm:create:win": "npm run typeorm:win migration:create -n", 28 | "typeorm:run:win": "npm run typeorm:win migration:run", 29 | "typeorm:revert:win": "npm run typeorm:win migration:revert" 30 | }, 31 | "dependencies": { 32 | "@fastify/cookie": "^9.4.0", 33 | "@fastify/static": "^7.0.4", 34 | "@nestjs/common": "^10.4.4", 35 | "@nestjs/config": "^3.2.3", 36 | "@nestjs/core": "^10.4.4", 37 | "@nestjs/jwt": "^10.2.0", 38 | "@nestjs/passport": "^10.0.3", 39 | "@nestjs/platform-fastify": "^10.4.4", 40 | "@nestjs/swagger": "^7.4.2", 41 | "@nestjs/typeorm": "^10.0.2", 42 | "bcrypt": "^5.0.1", 43 | "class-transformer": "^0.4.0", 44 | "class-validator": "^0.13.1", 45 | "cookie-parser": "^1.4.6", 46 | "fastify": "^4.28.1", 47 | "passport": "^0.5.2", 48 | "passport-jwt": "^4.0.0", 49 | "passport-local": "^1.0.0", 50 | "pg": "^8.5.1", 51 | "reflect-metadata": "^0.2.0", 52 | "rimraf": "^3.0.2", 53 | "rxjs": "^7.8.1", 54 | "typeorm": "^0.3.20" 55 | }, 56 | "devDependencies": { 57 | "@nestjs/cli": "^10.4.5", 58 | "@nestjs/schematics": "^10.1.4", 59 | "@nestjs/testing": "^10.4.4", 60 | "@types/bcrypt": "^5.0.0", 61 | "@types/cookie-parser": "^1.4.2", 62 | "@types/jest": "^29.5.2", 63 | "@types/node": "^20.3.1", 64 | "@types/passport-jwt": "^3.0.6", 65 | "@types/passport-local": "^1.0.34", 66 | "@types/supertest": "^6.0.0", 67 | "@typescript-eslint/eslint-plugin": "^8.0.0", 68 | "@typescript-eslint/parser": "^8.0.0", 69 | "eslint": "^8.42.0", 70 | "eslint-config-prettier": "^9.0.0", 71 | "eslint-plugin-prettier": "^5.0.0", 72 | "jest": "^29.5.0", 73 | "prettier": "^3.0.0", 74 | "source-map-support": "^0.5.21", 75 | "supertest": "^7.0.0", 76 | "ts-jest": "^29.1.0", 77 | "ts-loader": "^9.4.3", 78 | "ts-node": "^10.9.1", 79 | "tsconfig-paths": "^4.2.0", 80 | "typescript": "^5.1.3" 81 | }, 82 | "jest": { 83 | "moduleFileExtensions": [ 84 | "js", 85 | "json", 86 | "ts" 87 | ], 88 | "rootDir": "src", 89 | "testRegex": ".*\\.spec\\.ts$", 90 | "transform": { 91 | "^.+\\.(t|j)s$": "ts-jest" 92 | }, 93 | "collectCoverageFrom": [ 94 | "**/*.(t|j)s" 95 | ], 96 | "coverageDirectory": "../coverage", 97 | "testEnvironment": "node" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { LoggerModule } from './infrastructure/logger/logger.module'; 5 | import { ExceptionsModule } from './infrastructure/exceptions/exceptions.module'; 6 | import { UsecasesProxyModule } from './infrastructure/usecases-proxy/usecases-proxy.module'; 7 | import { ControllersModule } from './infrastructure/controllers/controllers.module'; 8 | import { BcryptModule } from './infrastructure/services/bcrypt/bcrypt.module'; 9 | import { JwtModule as JwtServiceModule } from './infrastructure/services/jwt/jwt.module'; 10 | import { EnvironmentConfigModule } from './infrastructure/config/environment-config/environment-config.module'; 11 | import { LocalStrategy } from './infrastructure/common/strategies/local.strategy'; 12 | import { JwtStrategy } from './infrastructure/common/strategies/jwt.strategy'; 13 | import { JwtRefreshTokenStrategy } from './infrastructure/common/strategies/jwtRefresh.strategy'; 14 | 15 | @Module({ 16 | imports: [ 17 | PassportModule, 18 | JwtModule.register({ 19 | secret: process.env.secret, 20 | }), 21 | LoggerModule, 22 | ExceptionsModule, 23 | UsecasesProxyModule.register(), 24 | ControllersModule, 25 | BcryptModule, 26 | JwtServiceModule, 27 | EnvironmentConfigModule, 28 | ], 29 | providers: [LocalStrategy, JwtStrategy, JwtRefreshTokenStrategy], 30 | }) 31 | export class AppModule {} 32 | -------------------------------------------------------------------------------- /src/domain/adapters/bcrypt.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IBcryptService { 2 | hash(hashString: string): Promise; 3 | compare(password: string, hashPassword: string): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/domain/adapters/jwt.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IJwtServicePayload { 2 | username: string; 3 | } 4 | 5 | export interface IJwtService { 6 | checkToken(token: string): Promise; 7 | createToken(payload: IJwtServicePayload, secret: string, expiresIn: string): string; 8 | } 9 | -------------------------------------------------------------------------------- /src/domain/config/database.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DatabaseConfig { 2 | getDatabaseHost(): string; 3 | getDatabasePort(): number; 4 | getDatabaseUser(): string; 5 | getDatabasePassword(): string; 6 | getDatabaseName(): string; 7 | getDatabaseSchema(): string; 8 | getDatabaseSync(): boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/config/jwt.interface.ts: -------------------------------------------------------------------------------- 1 | export interface JWTConfig { 2 | getJwtSecret(): string; 3 | getJwtExpirationTime(): string; 4 | getJwtRefreshSecret(): string; 5 | getJwtRefreshExpirationTime(): string; 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/exceptions/exceptions.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IFormatExceptionMessage { 2 | message: string; 3 | code_error?: number; 4 | } 5 | 6 | export interface IException { 7 | badRequestException(data: IFormatExceptionMessage): void; 8 | internalServerErrorException(data?: IFormatExceptionMessage): void; 9 | forbiddenException(data?: IFormatExceptionMessage): void; 10 | UnauthorizedException(data?: IFormatExceptionMessage): void; 11 | } 12 | -------------------------------------------------------------------------------- /src/domain/logger/logger.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | debug(context: string, message: string): void; 3 | log(context: string, message: string): void; 4 | error(context: string, message: string, trace?: string): void; 5 | warn(context: string, message: string): void; 6 | verbose(context: string, message: string): void; 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/model/auth.ts: -------------------------------------------------------------------------------- 1 | export interface TokenPayload { 2 | username: string; 3 | userId: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/domain/model/todo.ts: -------------------------------------------------------------------------------- 1 | export class TodoM { 2 | id: number; 3 | content: string; 4 | isDone: boolean; 5 | createdDate: Date; 6 | updatedDate: Date; 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/model/user.ts: -------------------------------------------------------------------------------- 1 | export class UserWithoutPassword { 2 | id: number; 3 | username: string; 4 | createDate: Date; 5 | updatedDate: Date; 6 | lastLogin: Date; 7 | hashRefreshToken: string; 8 | } 9 | 10 | export class UserM extends UserWithoutPassword { 11 | password: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/domain/repositories/todoRepository.interface.ts: -------------------------------------------------------------------------------- 1 | import { TodoM } from '../model/todo'; 2 | 3 | export interface TodoRepository { 4 | insert(todo: TodoM): Promise; 5 | findAll(): Promise; 6 | findById(id: number): Promise; 7 | updateContent(id: number, isDone: boolean): Promise; 8 | deleteById(id: number): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/repositories/userRepository.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserM } from '../model/user'; 2 | 3 | export interface UserRepository { 4 | getUserByUsername(username: string): Promise; 5 | updateLastLogin(username: string): Promise; 6 | updateRefreshToken(username: string, refreshToken: string): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/infrastructure/common/filter/exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; 2 | import { FastifyRequest, FastifyReply } from 'fastify'; 3 | import { LoggerService } from '../../logger/logger.service'; 4 | 5 | interface IError { 6 | message: string; 7 | code_error: string; 8 | } 9 | 10 | @Catch() 11 | export class AllExceptionFilter implements ExceptionFilter { 12 | constructor(private readonly logger: LoggerService) {} 13 | catch(exception: any, host: ArgumentsHost) { 14 | const ctx = host.switchToHttp(); 15 | const response = ctx.getResponse(); 16 | const request = ctx.getRequest(); 17 | 18 | const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; 19 | const message = 20 | exception instanceof HttpException 21 | ? (exception.getResponse() as IError) 22 | : { message: (exception as Error).message, code_error: null }; 23 | 24 | const responseData = { 25 | ...{ 26 | statusCode: status, 27 | timestamp: new Date().toISOString(), 28 | path: request.url, 29 | }, 30 | ...message, 31 | }; 32 | 33 | this.logMessage(request, message, status, exception); 34 | 35 | response.status(status).send(responseData); 36 | } 37 | 38 | private logMessage(request: FastifyRequest, message: IError, status: number, exception: any) { 39 | if (status === 500) { 40 | this.logger.error( 41 | `End Request for ${request.url}`, 42 | `method=${request.method} status=${status} code_error=${ 43 | message.code_error ? message.code_error : null 44 | } message=${message.message ? message.message : null}`, 45 | status >= 500 ? exception.stack : '', 46 | ); 47 | } else { 48 | this.logger.warn( 49 | `End Request for ${request.url}`, 50 | `method=${request.method} status=${status} code_error=${ 51 | message.code_error ? message.code_error : null 52 | } message=${message.message ? message.message : null}`, 53 | ); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/infrastructure/common/guards/jwtAuth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class JwtAuthGuard extends AuthGuard('jwt') {} 6 | -------------------------------------------------------------------------------- /src/infrastructure/common/guards/jwtRefresh.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {} 6 | -------------------------------------------------------------------------------- /src/infrastructure/common/guards/login.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LoginGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/infrastructure/common/interceptors/logger.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { FastifyRequest } from 'fastify'; 3 | import { Observable } from 'rxjs'; 4 | import { tap } from 'rxjs/operators'; 5 | import { LoggerService } from '../../logger/logger.service'; 6 | 7 | @Injectable() 8 | export class LoggingInterceptor implements NestInterceptor { 9 | constructor(private readonly logger: LoggerService) {} 10 | 11 | intercept(context: ExecutionContext, next: CallHandler): Observable { 12 | const now = Date.now(); 13 | const httpContext = context.switchToHttp(); 14 | const request = httpContext.getRequest(); 15 | 16 | const ip = this.getIP(request); 17 | 18 | this.logger.log(`Incoming Request on ${request.url}`, `method=${request.method} ip=${ip}`); 19 | 20 | return next.handle().pipe( 21 | tap(() => { 22 | this.logger.log(`End Request for ${request.url}`, `method=${request.method} ip=${ip} duration=${Date.now() - now}ms`); 23 | }), 24 | ); 25 | } 26 | 27 | private getIP(request: FastifyRequest): string { 28 | let ip: string; 29 | const ipAddr = request.headers['x-forwarded-for']; 30 | if (ipAddr) { 31 | ip = ipAddr[ipAddr.length - 1]; 32 | } else { 33 | ip = request.socket.remoteAddress; 34 | } 35 | return ip.replace('::ffff:', ''); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/infrastructure/common/interceptors/response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2 | import { FastifyRequest } from 'fastify'; 3 | import { ApiProperty } from '@nestjs/swagger'; 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | export class ResponseFormat { 8 | @ApiProperty() 9 | isArray: boolean; 10 | @ApiProperty() 11 | path: string; 12 | @ApiProperty() 13 | duration: string; 14 | @ApiProperty() 15 | method: string; 16 | 17 | data: T; 18 | } 19 | 20 | @Injectable() 21 | export class ResponseInterceptor implements NestInterceptor> { 22 | intercept(context: ExecutionContext, next: CallHandler): Observable> { 23 | const now = Date.now(); 24 | const httpContext = context.switchToHttp(); 25 | const request = httpContext.getRequest(); 26 | 27 | return next.handle().pipe( 28 | map((data) => ({ 29 | data, 30 | isArray: Array.isArray(data), 31 | path: request.url, 32 | duration: `${Date.now() - now}ms`, 33 | method: request.method, 34 | })), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/infrastructure/common/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Inject, Injectable } from '@nestjs/common'; 4 | import { Request } from 'express'; 5 | import { UsecasesProxyModule } from '../../usecases-proxy/usecases-proxy.module'; 6 | import { UseCaseProxy } from '../../usecases-proxy/usecases-proxy'; 7 | import { LoginUseCases } from '../../../usecases/auth/login.usecases'; 8 | import { ExceptionsService } from '../../exceptions/exceptions.service'; 9 | import { LoggerService } from '../../logger/logger.service'; 10 | 11 | @Injectable() 12 | export class JwtStrategy extends PassportStrategy(Strategy) { 13 | constructor( 14 | @Inject(UsecasesProxyModule.LOGIN_USECASES_PROXY) 15 | private readonly loginUsecaseProxy: UseCaseProxy, 16 | private readonly logger: LoggerService, 17 | private readonly exceptionService: ExceptionsService, 18 | ) { 19 | super({ 20 | jwtFromRequest: ExtractJwt.fromExtractors([ 21 | (request: Request) => { 22 | return request.cookies.accessToken; 23 | }, 24 | ]), 25 | secretOrKey: process.env.JWT_SECRET, 26 | }); 27 | } 28 | 29 | async validate(payload: any) { 30 | const user = await this.loginUsecaseProxy.getInstance().validateUserForJWTStragtegy(payload.username); 31 | if (!user) { 32 | this.logger.warn('JwtStrategy', `User not found`); 33 | this.exceptionService.UnauthorizedException({ message: 'User not found' }); 34 | } 35 | return user; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/infrastructure/common/strategies/jwtRefresh.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Inject, Injectable } from '@nestjs/common'; 4 | import { Request } from 'express'; 5 | import { EnvironmentConfigService } from '../../config/environment-config/environment-config.service'; 6 | import { UsecasesProxyModule } from '../../usecases-proxy/usecases-proxy.module'; 7 | import { UseCaseProxy } from '../../usecases-proxy/usecases-proxy'; 8 | import { LoginUseCases } from '../../../usecases/auth/login.usecases'; 9 | import { TokenPayload } from '../../../domain/model/auth'; 10 | import { LoggerService } from '../../logger/logger.service'; 11 | import { ExceptionsService } from '../../exceptions/exceptions.service'; 12 | 13 | @Injectable() 14 | export class JwtRefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh-token') { 15 | constructor( 16 | private readonly configService: EnvironmentConfigService, 17 | @Inject(UsecasesProxyModule.LOGIN_USECASES_PROXY) 18 | private readonly loginUsecaseProxy: UseCaseProxy, 19 | private readonly logger: LoggerService, 20 | private readonly exceptionService: ExceptionsService, 21 | ) { 22 | super({ 23 | jwtFromRequest: ExtractJwt.fromExtractors([ 24 | (request: Request) => { 25 | return request?.cookies?.refreshToken; 26 | }, 27 | ]), 28 | secretOrKey: configService.getJwtRefreshSecret(), 29 | passReqToCallback: true, 30 | }); 31 | } 32 | 33 | async validate(request: Request, payload: TokenPayload) { 34 | const refreshToken = request.cookies?.refreshToken; 35 | const user = this.loginUsecaseProxy.getInstance().getUserIfRefreshTokenMatches(refreshToken, payload.username); 36 | if (!user) { 37 | this.logger.warn('JwtStrategy', `User not found or hash not correct`); 38 | this.exceptionService.UnauthorizedException({ message: 'User not found or hash not correct' }); 39 | } 40 | return user; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/infrastructure/common/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Inject, Injectable } from '@nestjs/common'; 4 | import { UsecasesProxyModule } from '../../usecases-proxy/usecases-proxy.module'; 5 | import { UseCaseProxy } from '../../usecases-proxy/usecases-proxy'; 6 | import { LoginUseCases } from '../../../usecases/auth/login.usecases'; 7 | import { LoggerService } from '../../logger/logger.service'; 8 | import { ExceptionsService } from '../../exceptions/exceptions.service'; 9 | 10 | @Injectable() 11 | export class LocalStrategy extends PassportStrategy(Strategy) { 12 | constructor( 13 | @Inject(UsecasesProxyModule.LOGIN_USECASES_PROXY) 14 | private readonly loginUsecaseProxy: UseCaseProxy, 15 | private readonly logger: LoggerService, 16 | private readonly exceptionService: ExceptionsService, 17 | ) { 18 | super(); 19 | } 20 | 21 | async validate(username: string, password: string) { 22 | if (!username || !password) { 23 | this.logger.warn('LocalStrategy', `Username or password is missing, BadRequestException`); 24 | this.exceptionService.UnauthorizedException(); 25 | } 26 | const user = await this.loginUsecaseProxy.getInstance().validateUserForLocalStragtegy(username, password); 27 | if (!user) { 28 | this.logger.warn('LocalStrategy', `Invalid username or password`); 29 | this.exceptionService.UnauthorizedException({ message: 'Invalid username or password.' }); 30 | } 31 | return user; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/infrastructure/common/swagger/response.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, Type } from '@nestjs/common'; 2 | import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'; 3 | import { ResponseFormat } from '../../common/interceptors/response.interceptor'; 4 | 5 | export const ApiResponseType = >(model: TModel, isArray: boolean) => { 6 | return applyDecorators( 7 | ApiOkResponse({ 8 | isArray: isArray, 9 | schema: { 10 | allOf: [ 11 | { $ref: getSchemaPath(ResponseFormat) }, 12 | { 13 | properties: { 14 | data: { 15 | $ref: getSchemaPath(model), 16 | }, 17 | isArray: { 18 | type: 'boolean', 19 | default: isArray, 20 | }, 21 | }, 22 | }, 23 | ], 24 | }, 25 | }), 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/infrastructure/config/environment-config/environment-config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { EnvironmentConfigService } from './environment-config.service'; 4 | import { validate } from './environment-config.validation'; 5 | 6 | @Module({ 7 | imports: [ 8 | ConfigModule.forRoot({ 9 | envFilePath: './env/local.env', 10 | ignoreEnvFile: !(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test'), 11 | isGlobal: true, 12 | validate, 13 | }), 14 | ], 15 | providers: [EnvironmentConfigService], 16 | exports: [EnvironmentConfigService], 17 | }) 18 | export class EnvironmentConfigModule {} 19 | -------------------------------------------------------------------------------- /src/infrastructure/config/environment-config/environment-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { DatabaseConfig } from '../../../domain/config/database.interface'; 4 | import { JWTConfig } from '../../../domain/config/jwt.interface'; 5 | 6 | @Injectable() 7 | export class EnvironmentConfigService implements DatabaseConfig, JWTConfig { 8 | constructor(private configService: ConfigService) {} 9 | 10 | getJwtSecret(): string { 11 | return this.configService.get('JWT_SECRET'); 12 | } 13 | 14 | getJwtExpirationTime(): string { 15 | return this.configService.get('JWT_EXPIRATION_TIME'); 16 | } 17 | 18 | getJwtRefreshSecret(): string { 19 | return this.configService.get('JWT_REFRESH_TOKEN_SECRET'); 20 | } 21 | 22 | getJwtRefreshExpirationTime(): string { 23 | return this.configService.get('JWT_REFRESH_TOKEN_EXPIRATION_TIME'); 24 | } 25 | 26 | getDatabaseHost(): string { 27 | return this.configService.get('DATABASE_HOST'); 28 | } 29 | 30 | getDatabasePort(): number { 31 | return this.configService.get('DATABASE_PORT'); 32 | } 33 | 34 | getDatabaseUser(): string { 35 | return this.configService.get('DATABASE_USER'); 36 | } 37 | 38 | getDatabasePassword(): string { 39 | return this.configService.get('DATABASE_PASSWORD'); 40 | } 41 | 42 | getDatabaseName(): string { 43 | return this.configService.get('DATABASE_NAME'); 44 | } 45 | 46 | getDatabaseSchema(): string { 47 | return this.configService.get('DATABASE_SCHEMA'); 48 | } 49 | 50 | getDatabaseSync(): boolean { 51 | return this.configService.get('DATABASE_SYNCHRONIZE'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/infrastructure/config/environment-config/environment-config.validation.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from 'class-transformer'; 2 | import { IsBoolean, IsEnum, IsNumber, IsString, validateSync } from 'class-validator'; 3 | 4 | enum Environment { 5 | Development = 'development', 6 | Production = 'production', 7 | Local = 'local', 8 | Test = 'test', 9 | } 10 | 11 | class EnvironmentVariables { 12 | @IsEnum(Environment) 13 | NODE_ENV: Environment; 14 | 15 | @IsString() 16 | JWT_SECRET: string; 17 | @IsString() 18 | JWT_EXPIRATION_TIME: string; 19 | @IsString() 20 | JWT_REFRESH_TOKEN_SECRET: string; 21 | @IsString() 22 | JWT_REFRESH_TOKEN_EXPIRATION_TIME: string; 23 | 24 | 25 | @IsString() 26 | DATABASE_HOST: string; 27 | @IsNumber() 28 | DATABASE_PORT: number; 29 | @IsString() 30 | DATABASE_USER: string; 31 | @IsString() 32 | DATABASE_PASSWORD: string; 33 | @IsString() 34 | DATABASE_NAME: string; 35 | @IsString() 36 | DATABASE_SCHEMA: string; 37 | @IsBoolean() 38 | DATABASE_SYNCHRONIZE: boolean; 39 | } 40 | 41 | export function validate(config: Record) { 42 | const validatedConfig = plainToClass(EnvironmentVariables, config, { 43 | enableImplicitConversion: true, 44 | }); 45 | const errors = validateSync(validatedConfig, { skipMissingProperties: false }); 46 | 47 | if (errors.length > 0) { 48 | throw new Error(errors.toString()); 49 | } 50 | return validatedConfig; 51 | } 52 | -------------------------------------------------------------------------------- /src/infrastructure/config/typeorm/typeorm.config.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceOptions } from 'typeorm'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | if (process.env.NODE_ENV === 'local') { 5 | dotenv.config({ path: './env/local.env' }); 6 | } 7 | 8 | const config: DataSourceOptions = { 9 | type: 'postgres', 10 | host: process.env.DATABASE_HOST, 11 | port: parseInt(process.env.DATABASE_PORT), 12 | username: process.env.DATABASE_USER, 13 | password: process.env.DATABASE_PASSWORD, 14 | database: process.env.DATABASE_NAME, 15 | entities: [__dirname + './../../**/*.entity{.ts,.js}'], 16 | synchronize: false, 17 | schema: process.env.DATABASE_SCHEMA, 18 | migrationsRun: true, 19 | migrationsTableName: 'migration_todo', 20 | migrations: ['database/migrations/**/*{.ts,.js}'], 21 | // ssl: { 22 | // rejectUnauthorized: false, 23 | // }, 24 | }; 25 | 26 | console.log(config); 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /src/infrastructure/config/typeorm/typeorm.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; 3 | import { EnvironmentConfigModule } from '../environment-config/environment-config.module'; 4 | import { EnvironmentConfigService } from '../environment-config/environment-config.service'; 5 | 6 | export const getTypeOrmModuleOptions = (config: EnvironmentConfigService): TypeOrmModuleOptions => 7 | ({ 8 | type: 'postgres', 9 | host: config.getDatabaseHost(), 10 | port: config.getDatabasePort(), 11 | username: config.getDatabaseUser(), 12 | password: config.getDatabasePassword(), 13 | database: config.getDatabaseName(), 14 | entities: [__dirname + './../../**/*.entity{.ts,.js}'], 15 | synchronize: false, 16 | schema: process.env.DATABASE_SCHEMA, 17 | migrationsRun: true, 18 | migrations: [__dirname + '/migrations/**/*{.ts,.js}'], 19 | cli: { 20 | migrationsDir: 'src/migrations', 21 | }, 22 | // ssl: { 23 | // rejectUnauthorized: false, 24 | // }, 25 | } as TypeOrmModuleOptions); 26 | 27 | @Module({ 28 | imports: [ 29 | TypeOrmModule.forRootAsync({ 30 | imports: [EnvironmentConfigModule], 31 | inject: [EnvironmentConfigService], 32 | useFactory: getTypeOrmModuleOptions, 33 | }), 34 | ], 35 | }) 36 | export class TypeOrmConfigModule {} 37 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/auth/auth-dto.class.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsString } from 'class-validator'; 3 | 4 | export class AuthLoginDto { 5 | @ApiProperty({ required: true }) 6 | @IsNotEmpty() 7 | @IsString() 8 | readonly username: string; 9 | 10 | @ApiProperty({ required: true }) 11 | @IsNotEmpty() 12 | @IsString() 13 | readonly password: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Inject, Post, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { ApiBearerAuth, ApiBody, ApiExtraModels, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { FastifyReply } from 'fastify'; 4 | 5 | import { AuthLoginDto } from './auth-dto.class'; 6 | import { IsAuthPresenter } from './auth.presenter'; 7 | 8 | import JwtRefreshGuard from '../../common/guards/jwtRefresh.guard'; 9 | import { JwtAuthGuard } from '../../common/guards/jwtAuth.guard'; 10 | import { LoginGuard } from '../../common/guards/login.guard'; 11 | 12 | import { UseCaseProxy } from '../../usecases-proxy/usecases-proxy'; 13 | import { UsecasesProxyModule } from '../../usecases-proxy/usecases-proxy.module'; 14 | import { LoginUseCases } from '../../../usecases/auth/login.usecases'; 15 | import { IsAuthenticatedUseCases } from '../../../usecases/auth/isAuthenticated.usecases'; 16 | import { LogoutUseCases } from '../../../usecases/auth/logout.usecases'; 17 | 18 | import { ApiResponseType } from '../../common/swagger/response.decorator'; 19 | 20 | @Controller('auth') 21 | @ApiTags('auth') 22 | @ApiResponse({ 23 | status: 401, 24 | description: 'No authorization token was found', 25 | }) 26 | @ApiResponse({ status: 500, description: 'Internal error' }) 27 | @ApiExtraModels(IsAuthPresenter) 28 | export class AuthController { 29 | constructor( 30 | @Inject(UsecasesProxyModule.LOGIN_USECASES_PROXY) 31 | private readonly loginUsecaseProxy: UseCaseProxy, 32 | @Inject(UsecasesProxyModule.LOGOUT_USECASES_PROXY) 33 | private readonly logoutUsecaseProxy: UseCaseProxy, 34 | @Inject(UsecasesProxyModule.IS_AUTHENTICATED_USECASES_PROXY) 35 | private readonly isAuthUsecaseProxy: UseCaseProxy, 36 | ) {} 37 | 38 | @Post('login') 39 | @UseGuards(LoginGuard) 40 | @ApiBearerAuth() 41 | @ApiBody({ type: AuthLoginDto }) 42 | @ApiOperation({ description: 'login' }) 43 | async login(@Body() auth: AuthLoginDto, @Res({ passthrough: true }) response: FastifyReply) { 44 | const accessTokenInfo = await this.loginUsecaseProxy.getInstance().getCookieWithJwtToken(auth.username); 45 | const refreshTokenInfo = await this.loginUsecaseProxy.getInstance().getCookieWithJwtRefreshToken(auth.username); 46 | 47 | response.setCookie('accessToken', accessTokenInfo.token, { 48 | path: '/', 49 | httpOnly: true, 50 | secure: true, 51 | maxAge: Number(accessTokenInfo.maxAge), 52 | }); 53 | response.setCookie('refreshToken', refreshTokenInfo.token, { 54 | path: '/', 55 | httpOnly: true, 56 | secure: true, 57 | maxAge: Number(refreshTokenInfo.maxAge), 58 | }); 59 | return 'Login successful'; 60 | } 61 | 62 | @Post('logout') 63 | @UseGuards(JwtAuthGuard) 64 | @ApiOperation({ description: 'logout' }) 65 | async logout(@Res({ passthrough: true }) response: FastifyReply) { 66 | response.clearCookie('accessToken'); 67 | response.clearCookie('refreshToken'); 68 | return 'Logout successful'; 69 | } 70 | 71 | @Get('is_authenticated') 72 | @ApiBearerAuth() 73 | @UseGuards(JwtAuthGuard) 74 | @ApiOperation({ description: 'is_authenticated' }) 75 | @ApiResponseType(IsAuthPresenter, false) 76 | async isAuthenticated(@Req() request: any) { 77 | const user = await this.isAuthUsecaseProxy.getInstance().execute(request.user.username); 78 | const response = new IsAuthPresenter(); 79 | response.username = user.username; 80 | return response; 81 | } 82 | 83 | @Get('refresh') 84 | @UseGuards(JwtRefreshGuard) 85 | @ApiBearerAuth() 86 | async refresh(@Req() request: any, @Res({ passthrough: true }) response: FastifyReply) { 87 | const accessTokenInfo = await this.loginUsecaseProxy.getInstance().getCookieWithJwtToken(request.user.username); 88 | response.setCookie('accessToken', accessTokenInfo.token, { 89 | path: '/', 90 | httpOnly: true, 91 | secure: true, 92 | maxAge: Number(accessTokenInfo.maxAge), 93 | }); 94 | return 'Refresh successful'; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/auth/auth.presenter.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class IsAuthPresenter { 4 | @ApiProperty() 5 | username: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/controllers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsecasesProxyModule } from '../usecases-proxy/usecases-proxy.module'; 3 | import { AuthController } from './auth/auth.controller'; 4 | import { TodoController } from './todo/todo.controller'; 5 | 6 | @Module({ 7 | imports: [UsecasesProxyModule.register()], 8 | controllers: [TodoController, AuthController], 9 | }) 10 | export class ControllersModule {} 11 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/todo/todo.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Inject, ParseIntPipe, Post, Put, Query } from '@nestjs/common'; 2 | import { ApiExtraModels, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { UseCaseProxy } from '../../usecases-proxy/usecases-proxy'; 4 | import { UsecasesProxyModule } from '../../usecases-proxy/usecases-proxy.module'; 5 | import { GetTodoUseCases } from '../../../usecases/todo/getTodo.usecases'; 6 | import { TodoPresenter } from './todo.presenter'; 7 | import { ApiResponseType } from '../../common/swagger/response.decorator'; 8 | import { getTodosUseCases } from '../../../usecases/todo/getTodos.usecases'; 9 | import { updateTodoUseCases } from '../../../usecases/todo/updateTodo.usecases'; 10 | import { AddTodoDto, UpdateTodoDto } from './todo.dto'; 11 | import { deleteTodoUseCases } from '../../../usecases/todo/deleteTodo.usecases'; 12 | import { addTodoUseCases } from '../../../usecases/todo/addTodo.usecases'; 13 | 14 | @Controller('todo') 15 | @ApiTags('todo') 16 | @ApiResponse({ status: 500, description: 'Internal error' }) 17 | @ApiExtraModels(TodoPresenter) 18 | export class TodoController { 19 | constructor( 20 | @Inject(UsecasesProxyModule.GET_TODO_USECASES_PROXY) 21 | private readonly getTodoUsecaseProxy: UseCaseProxy, 22 | @Inject(UsecasesProxyModule.GET_TODOS_USECASES_PROXY) 23 | private readonly getAllTodoUsecaseProxy: UseCaseProxy, 24 | @Inject(UsecasesProxyModule.PUT_TODO_USECASES_PROXY) 25 | private readonly updateTodoUsecaseProxy: UseCaseProxy, 26 | @Inject(UsecasesProxyModule.DELETE_TODO_USECASES_PROXY) 27 | private readonly deleteTodoUsecaseProxy: UseCaseProxy, 28 | @Inject(UsecasesProxyModule.POST_TODO_USECASES_PROXY) 29 | private readonly addTodoUsecaseProxy: UseCaseProxy, 30 | ) {} 31 | 32 | @Get('todo') 33 | @ApiResponseType(TodoPresenter, false) 34 | async getTodo(@Query('id', ParseIntPipe) id: number) { 35 | const todo = await this.getTodoUsecaseProxy.getInstance().execute(id); 36 | return new TodoPresenter(todo); 37 | } 38 | 39 | @Get('todos') 40 | @ApiResponseType(TodoPresenter, true) 41 | async getTodos() { 42 | const todos = await this.getAllTodoUsecaseProxy.getInstance().execute(); 43 | return todos.map((todo) => new TodoPresenter(todo)); 44 | } 45 | 46 | @Put('todo') 47 | @ApiResponseType(TodoPresenter, true) 48 | async updateTodo(@Body() updateTodoDto: UpdateTodoDto) { 49 | const { id, isDone } = updateTodoDto; 50 | await this.updateTodoUsecaseProxy.getInstance().execute(id, isDone); 51 | return 'success'; 52 | } 53 | 54 | @Delete('todo') 55 | @ApiResponseType(TodoPresenter, true) 56 | async deleteTodo(@Query('id', ParseIntPipe) id: number) { 57 | await this.deleteTodoUsecaseProxy.getInstance().execute(id); 58 | return 'success'; 59 | } 60 | 61 | @Post('todo') 62 | @ApiResponseType(TodoPresenter, true) 63 | async addTodo(@Body() addTodoDto: AddTodoDto) { 64 | const { content } = addTodoDto; 65 | const todoCreated = await this.addTodoUsecaseProxy.getInstance().execute(content); 66 | return new TodoPresenter(todoCreated); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/todo/todo.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; 3 | 4 | export class UpdateTodoDto { 5 | @ApiProperty({ required: true }) 6 | @IsNotEmpty() 7 | @IsNumber() 8 | readonly id: number; 9 | @ApiProperty({ required: true }) 10 | @IsNotEmpty() 11 | @IsBoolean() 12 | readonly isDone: boolean; 13 | } 14 | 15 | export class AddTodoDto { 16 | @ApiProperty({ required: true }) 17 | @IsNotEmpty() 18 | @IsString() 19 | readonly content: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/infrastructure/controllers/todo/todo.presenter.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { TodoM } from '../../../domain/model/todo'; 3 | 4 | export class TodoPresenter { 5 | @ApiProperty() 6 | id: number; 7 | @ApiProperty() 8 | content: string; 9 | @ApiProperty() 10 | isDone: boolean; 11 | @ApiProperty() 12 | createdate: Date; 13 | @ApiProperty() 14 | updateddate: Date; 15 | 16 | constructor(todo: TodoM) { 17 | this.id = todo.id; 18 | this.content = todo.content; 19 | this.isDone = todo.isDone; 20 | this.createdate = todo.createdDate; 21 | this.updateddate = todo.updatedDate; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/entities/todo.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Todo { 5 | @PrimaryGeneratedColumn({ type: 'integer' }) 6 | id: number; 7 | 8 | @Column('varchar', { length: 255, nullable: true }) 9 | content: string; 10 | 11 | @Column('boolean', { default: false }) 12 | is_done: boolean; 13 | 14 | @CreateDateColumn({ name: 'createdate' }) 15 | created_date: Date; 16 | 17 | @UpdateDateColumn({ name: 'updateddate' }) 18 | updated_date: Date; 19 | } 20 | -------------------------------------------------------------------------------- /src/infrastructure/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class User { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Index({ unique: true }) 9 | @Column('varchar', { unique: true }) 10 | username: string; 11 | 12 | @Column('text') 13 | password: string; 14 | 15 | @CreateDateColumn({ name: 'createdate' }) 16 | createdate: Date; 17 | 18 | @UpdateDateColumn({ name: 'updateddate' }) 19 | updateddate: Date; 20 | 21 | @Column({ nullable: true }) 22 | last_login?: Date; 23 | 24 | @Column('varchar', { nullable: true }) 25 | hach_refresh_token: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/infrastructure/exceptions/exceptions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ExceptionsService } from './exceptions.service'; 3 | 4 | @Module({ 5 | providers: [ExceptionsService], 6 | exports: [ExceptionsService], 7 | }) 8 | export class ExceptionsModule {} 9 | -------------------------------------------------------------------------------- /src/infrastructure/exceptions/exceptions.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | ForbiddenException, 4 | Injectable, 5 | InternalServerErrorException, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { IException, IFormatExceptionMessage } from '../../domain/exceptions/exceptions.interface'; 9 | 10 | @Injectable() 11 | export class ExceptionsService implements IException { 12 | badRequestException(data: IFormatExceptionMessage): void { 13 | throw new BadRequestException(data); 14 | } 15 | internalServerErrorException(data?: IFormatExceptionMessage): void { 16 | throw new InternalServerErrorException(data); 17 | } 18 | forbiddenException(data?: IFormatExceptionMessage): void { 19 | throw new ForbiddenException(data); 20 | } 21 | UnauthorizedException(data?: IFormatExceptionMessage): void { 22 | throw new UnauthorizedException(data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/infrastructure/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LoggerService } from './logger.service'; 3 | 4 | @Module({ 5 | providers: [LoggerService], 6 | exports: [LoggerService], 7 | }) 8 | export class LoggerModule {} 9 | -------------------------------------------------------------------------------- /src/infrastructure/logger/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { ILogger } from '../../domain/logger/logger.interface'; 3 | 4 | @Injectable() 5 | export class LoggerService extends Logger implements ILogger { 6 | debug(context: string, message: string) { 7 | if (process.env.NODE_ENV !== 'production') { 8 | super.debug(`[DEBUG] ${message}`, context); 9 | } 10 | } 11 | log(context: string, message: string) { 12 | super.log(`[INFO] ${message}`, context); 13 | } 14 | error(context: string, message: string, trace?: string) { 15 | super.error(`[ERROR] ${message}`, trace, context); 16 | } 17 | warn(context: string, message: string) { 18 | super.warn(`[WARN] ${message}`, context); 19 | } 20 | verbose(context: string, message: string) { 21 | if (process.env.NODE_ENV !== 'production') { 22 | super.verbose(`[VERBOSE] ${message}`, context); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/infrastructure/repositories/repositories.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { TypeOrmConfigModule } from '../config/typeorm/typeorm.module'; 4 | import { Todo } from '../entities/todo.entity'; 5 | import { User } from '../entities/user.entity'; 6 | import { DatabaseTodoRepository } from './todo.repository'; 7 | import { DatabaseUserRepository } from './user.repository'; 8 | 9 | @Module({ 10 | imports: [TypeOrmConfigModule, TypeOrmModule.forFeature([Todo, User])], 11 | providers: [DatabaseTodoRepository, DatabaseUserRepository], 12 | exports: [DatabaseTodoRepository, DatabaseUserRepository], 13 | }) 14 | export class RepositoriesModule {} 15 | -------------------------------------------------------------------------------- /src/infrastructure/repositories/todo.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { TodoM } from '../../domain/model/todo'; 5 | import { TodoRepository } from '../../domain/repositories/todoRepository.interface'; 6 | import { Todo } from '../entities/todo.entity'; 7 | 8 | @Injectable() 9 | export class DatabaseTodoRepository implements TodoRepository { 10 | constructor( 11 | @InjectRepository(Todo) 12 | private readonly todoEntityRepository: Repository, 13 | ) {} 14 | 15 | async updateContent(id: number, isDone: boolean): Promise { 16 | await this.todoEntityRepository.update( 17 | { 18 | id: id, 19 | }, 20 | { is_done: isDone }, 21 | ); 22 | } 23 | async insert(todo: TodoM): Promise { 24 | const todoEntity = this.toTodoEntity(todo); 25 | const result = await this.todoEntityRepository.insert(todoEntity); 26 | return this.toTodo(result.generatedMaps[0] as Todo); 27 | } 28 | async findAll(): Promise { 29 | const todosEntity = await this.todoEntityRepository.find(); 30 | return todosEntity.map((todoEntity) => this.toTodo(todoEntity)); 31 | } 32 | async findById(id: number): Promise { 33 | const todoEntity = await this.todoEntityRepository.findOneOrFail({ where: { id } }); 34 | return this.toTodo(todoEntity); 35 | } 36 | async deleteById(id: number): Promise { 37 | await this.todoEntityRepository.delete({ id: id }); 38 | } 39 | 40 | private toTodo(todoEntity: Todo): TodoM { 41 | const todo: TodoM = new TodoM(); 42 | 43 | todo.id = todoEntity.id; 44 | todo.content = todoEntity.content; 45 | todo.isDone = todoEntity.is_done; 46 | todo.createdDate = todoEntity.created_date; 47 | todo.updatedDate = todoEntity.updated_date; 48 | 49 | return todo; 50 | } 51 | 52 | private toTodoEntity(todo: TodoM): Todo { 53 | const todoEntity: Todo = new Todo(); 54 | 55 | todoEntity.id = todo.id; 56 | todoEntity.content = todo.content; 57 | todoEntity.is_done = todo.isDone; 58 | 59 | return todoEntity; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/infrastructure/repositories/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { UserM } from '../../domain/model/user'; 5 | import { UserRepository } from '../../domain/repositories/userRepository.interface'; 6 | import { User } from '../entities/user.entity'; 7 | 8 | @Injectable() 9 | export class DatabaseUserRepository implements UserRepository { 10 | constructor( 11 | @InjectRepository(User) 12 | private readonly userEntityRepository: Repository, 13 | ) {} 14 | async updateRefreshToken(username: string, refreshToken: string): Promise { 15 | await this.userEntityRepository.update( 16 | { 17 | username: username, 18 | }, 19 | { hach_refresh_token: refreshToken }, 20 | ); 21 | } 22 | async getUserByUsername(username: string): Promise { 23 | const adminUserEntity = await this.userEntityRepository.findOne({ 24 | where: { 25 | username: username, 26 | }, 27 | }); 28 | if (!adminUserEntity) { 29 | return null; 30 | } 31 | return this.toUser(adminUserEntity); 32 | } 33 | async updateLastLogin(username: string): Promise { 34 | await this.userEntityRepository.update( 35 | { 36 | username: username, 37 | }, 38 | { last_login: () => 'CURRENT_TIMESTAMP' }, 39 | ); 40 | } 41 | 42 | private toUser(adminUserEntity: User): UserM { 43 | const adminUser: UserM = new UserM(); 44 | 45 | adminUser.id = adminUserEntity.id; 46 | adminUser.username = adminUserEntity.username; 47 | adminUser.password = adminUserEntity.password; 48 | adminUser.createDate = adminUserEntity.createdate; 49 | adminUser.updatedDate = adminUserEntity.updateddate; 50 | adminUser.lastLogin = adminUserEntity.last_login; 51 | adminUser.hashRefreshToken = adminUserEntity.hach_refresh_token; 52 | 53 | return adminUser; 54 | } 55 | 56 | private toUserEntity(adminUser: UserM): User { 57 | const adminUserEntity: User = new User(); 58 | 59 | adminUserEntity.username = adminUser.username; 60 | adminUserEntity.password = adminUser.password; 61 | adminUserEntity.last_login = adminUser.lastLogin; 62 | 63 | return adminUserEntity; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/infrastructure/services/bcrypt/bcrypt.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BcryptService } from './bcrypt.service'; 3 | 4 | @Module({ 5 | providers: [BcryptService], 6 | exports: [BcryptService] 7 | }) 8 | export class BcryptModule {} 9 | -------------------------------------------------------------------------------- /src/infrastructure/services/bcrypt/bcrypt.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BcryptService } from './bcrypt.service'; 3 | 4 | describe('BcryptService', () => { 5 | let service: BcryptService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BcryptService], 10 | }).compile(); 11 | 12 | service = module.get(BcryptService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | 19 | it('should hash a password correctly', async () => { 20 | const passwordHashed = await service.hash('password'); 21 | 22 | expect(await service.compare('password', passwordHashed)).toBe(true); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/infrastructure/services/bcrypt/bcrypt.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as bcrypt from 'bcrypt'; 3 | import { IBcryptService } from '../../../domain/adapters/bcrypt.interface'; 4 | 5 | @Injectable() 6 | export class BcryptService implements IBcryptService { 7 | rounds: number = 10; 8 | 9 | async hash(hashString: string): Promise { 10 | return await bcrypt.hash(hashString, this.rounds); 11 | } 12 | 13 | async compare(password: string, hashPassword: string): Promise { 14 | return await bcrypt.compare(password, hashPassword); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/infrastructure/services/jwt/jwt.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule as Jwt } from '@nestjs/jwt'; 3 | import { JwtTokenService } from './jwt.service'; 4 | 5 | @Module({ 6 | imports: [ 7 | Jwt.register({ 8 | secret: process.env.JWT_SECRET, 9 | signOptions: { expiresIn: '24h' }, 10 | }), 11 | ], 12 | providers: [JwtTokenService], 13 | exports: [JwtTokenService], 14 | }) 15 | export class JwtModule {} 16 | -------------------------------------------------------------------------------- /src/infrastructure/services/jwt/jwt.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { IJwtService, IJwtServicePayload } from '../../../domain/adapters/jwt.interface'; 4 | 5 | @Injectable() 6 | export class JwtTokenService implements IJwtService { 7 | constructor(private readonly jwtService: JwtService) {} 8 | 9 | async checkToken(token: string): Promise { 10 | const decode = await this.jwtService.verifyAsync(token); 11 | return decode; 12 | } 13 | 14 | createToken(payload: IJwtServicePayload, secret: string, expiresIn: string): string { 15 | return this.jwtService.sign(payload, { 16 | secret: secret, 17 | expiresIn: expiresIn, 18 | }); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/infrastructure/usecases-proxy/usecases-proxy.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { addTodoUseCases } from '../../usecases/todo/addTodo.usecases'; 3 | import { deleteTodoUseCases } from '../../usecases/todo/deleteTodo.usecases'; 4 | import { GetTodoUseCases } from '../../usecases/todo/getTodo.usecases'; 5 | import { getTodosUseCases } from '../../usecases/todo/getTodos.usecases'; 6 | import { updateTodoUseCases } from '../../usecases/todo/updateTodo.usecases'; 7 | import { IsAuthenticatedUseCases } from '../../usecases/auth/isAuthenticated.usecases'; 8 | import { LoginUseCases } from '../../usecases/auth/login.usecases'; 9 | import { LogoutUseCases } from '../../usecases/auth/logout.usecases'; 10 | 11 | import { ExceptionsModule } from '../exceptions/exceptions.module'; 12 | import { LoggerModule } from '../logger/logger.module'; 13 | import { LoggerService } from '../logger/logger.service'; 14 | 15 | import { BcryptModule } from '../services/bcrypt/bcrypt.module'; 16 | import { BcryptService } from '../services/bcrypt/bcrypt.service'; 17 | import { JwtModule } from '../services/jwt/jwt.module'; 18 | import { JwtTokenService } from '../services/jwt/jwt.service'; 19 | import { RepositoriesModule } from '../repositories/repositories.module'; 20 | 21 | import { DatabaseTodoRepository } from '../repositories/todo.repository'; 22 | import { DatabaseUserRepository } from '../repositories/user.repository'; 23 | 24 | import { EnvironmentConfigModule } from '../config/environment-config/environment-config.module'; 25 | import { EnvironmentConfigService } from '../config/environment-config/environment-config.service'; 26 | import { UseCaseProxy } from './usecases-proxy'; 27 | 28 | @Module({ 29 | imports: [LoggerModule, JwtModule, BcryptModule, EnvironmentConfigModule, RepositoriesModule, ExceptionsModule], 30 | }) 31 | export class UsecasesProxyModule { 32 | // Auth 33 | static LOGIN_USECASES_PROXY = 'LoginUseCasesProxy'; 34 | static IS_AUTHENTICATED_USECASES_PROXY = 'IsAuthenticatedUseCasesProxy'; 35 | static LOGOUT_USECASES_PROXY = 'LogoutUseCasesProxy'; 36 | 37 | static GET_TODO_USECASES_PROXY = 'getTodoUsecasesProxy'; 38 | static GET_TODOS_USECASES_PROXY = 'getTodosUsecasesProxy'; 39 | static POST_TODO_USECASES_PROXY = 'postTodoUsecasesProxy'; 40 | static DELETE_TODO_USECASES_PROXY = 'deleteTodoUsecasesProxy'; 41 | static PUT_TODO_USECASES_PROXY = 'putTodoUsecasesProxy'; 42 | 43 | static register(): DynamicModule { 44 | return { 45 | module: UsecasesProxyModule, 46 | providers: [ 47 | { 48 | inject: [LoggerService, JwtTokenService, EnvironmentConfigService, DatabaseUserRepository, BcryptService], 49 | provide: UsecasesProxyModule.LOGIN_USECASES_PROXY, 50 | useFactory: ( 51 | logger: LoggerService, 52 | jwtTokenService: JwtTokenService, 53 | config: EnvironmentConfigService, 54 | userRepo: DatabaseUserRepository, 55 | bcryptService: BcryptService, 56 | ) => new UseCaseProxy(new LoginUseCases(logger, jwtTokenService, config, userRepo, bcryptService)), 57 | }, 58 | { 59 | inject: [DatabaseUserRepository], 60 | provide: UsecasesProxyModule.IS_AUTHENTICATED_USECASES_PROXY, 61 | useFactory: (userRepo: DatabaseUserRepository) => new UseCaseProxy(new IsAuthenticatedUseCases(userRepo)), 62 | }, 63 | { 64 | inject: [], 65 | provide: UsecasesProxyModule.LOGOUT_USECASES_PROXY, 66 | useFactory: () => new UseCaseProxy(new LogoutUseCases()), 67 | }, 68 | { 69 | inject: [DatabaseTodoRepository], 70 | provide: UsecasesProxyModule.GET_TODO_USECASES_PROXY, 71 | useFactory: (todoRepository: DatabaseTodoRepository) => new UseCaseProxy(new GetTodoUseCases(todoRepository)), 72 | }, 73 | { 74 | inject: [DatabaseTodoRepository], 75 | provide: UsecasesProxyModule.GET_TODOS_USECASES_PROXY, 76 | useFactory: (todoRepository: DatabaseTodoRepository) => new UseCaseProxy(new getTodosUseCases(todoRepository)), 77 | }, 78 | { 79 | inject: [LoggerService, DatabaseTodoRepository], 80 | provide: UsecasesProxyModule.POST_TODO_USECASES_PROXY, 81 | useFactory: (logger: LoggerService, todoRepository: DatabaseTodoRepository) => 82 | new UseCaseProxy(new addTodoUseCases(logger, todoRepository)), 83 | }, 84 | { 85 | inject: [LoggerService, DatabaseTodoRepository], 86 | provide: UsecasesProxyModule.PUT_TODO_USECASES_PROXY, 87 | useFactory: (logger: LoggerService, todoRepository: DatabaseTodoRepository) => 88 | new UseCaseProxy(new updateTodoUseCases(logger, todoRepository)), 89 | }, 90 | { 91 | inject: [LoggerService, DatabaseTodoRepository], 92 | provide: UsecasesProxyModule.DELETE_TODO_USECASES_PROXY, 93 | useFactory: (logger: LoggerService, todoRepository: DatabaseTodoRepository) => 94 | new UseCaseProxy(new deleteTodoUseCases(logger, todoRepository)), 95 | }, 96 | ], 97 | exports: [ 98 | UsecasesProxyModule.GET_TODO_USECASES_PROXY, 99 | UsecasesProxyModule.GET_TODOS_USECASES_PROXY, 100 | UsecasesProxyModule.POST_TODO_USECASES_PROXY, 101 | UsecasesProxyModule.PUT_TODO_USECASES_PROXY, 102 | UsecasesProxyModule.DELETE_TODO_USECASES_PROXY, 103 | UsecasesProxyModule.LOGIN_USECASES_PROXY, 104 | UsecasesProxyModule.IS_AUTHENTICATED_USECASES_PROXY, 105 | UsecasesProxyModule.LOGOUT_USECASES_PROXY, 106 | ], 107 | }; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/infrastructure/usecases-proxy/usecases-proxy.ts: -------------------------------------------------------------------------------- 1 | export class UseCaseProxy { 2 | constructor(private readonly useCase: T) {} 3 | getInstance(): T { 4 | return this.useCase; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe, VersioningType } from '@nestjs/common'; 2 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; 3 | import { NestFactory } from '@nestjs/core'; 4 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 5 | import fastifyCookie from '@fastify/cookie'; 6 | import { AppModule } from './app.module'; 7 | import { AllExceptionFilter } from './infrastructure/common/filter/exception.filter'; 8 | import { LoggingInterceptor } from './infrastructure/common/interceptors/logger.interceptor'; 9 | import { ResponseFormat, ResponseInterceptor } from './infrastructure/common/interceptors/response.interceptor'; 10 | import { LoggerService } from './infrastructure/logger/logger.service'; 11 | 12 | async function bootstrap() { 13 | const env = process.env.NODE_ENV; 14 | const app = await NestFactory.create(AppModule, new FastifyAdapter()); 15 | 16 | await app.register(fastifyCookie); 17 | 18 | // Filter 19 | app.useGlobalFilters(new AllExceptionFilter(new LoggerService())); 20 | 21 | // pipes 22 | app.useGlobalPipes(new ValidationPipe()); 23 | 24 | // interceptors 25 | app.useGlobalInterceptors(new LoggingInterceptor(new LoggerService())); 26 | app.useGlobalInterceptors(new ResponseInterceptor()); 27 | 28 | app.enableVersioning({ 29 | defaultVersion: '1', 30 | type: VersioningType.URI, 31 | }); 32 | 33 | // swagger config 34 | if (env !== 'production') { 35 | const config = new DocumentBuilder() 36 | .addBearerAuth() 37 | .setTitle('Clean Architecture Nestjs') 38 | .setDescription('Example with todo list') 39 | .setVersion('1') 40 | .build(); 41 | const document = SwaggerModule.createDocument(app, config, { 42 | extraModels: [ResponseFormat], 43 | deepScanRoutes: true, 44 | }); 45 | SwaggerModule.setup('api', app, document); 46 | } 47 | 48 | await app.listen(3000); 49 | } 50 | bootstrap(); 51 | -------------------------------------------------------------------------------- /src/usecases/auth/isAuthenticated.usecases.ts: -------------------------------------------------------------------------------- 1 | import { UserM, UserWithoutPassword } from '../../domain/model/user'; 2 | import { UserRepository } from '../../domain/repositories/userRepository.interface'; 3 | 4 | export class IsAuthenticatedUseCases { 5 | constructor(private readonly adminUserRepo: UserRepository) {} 6 | 7 | async execute(username: string): Promise { 8 | const user: UserM = await this.adminUserRepo.getUserByUsername(username); 9 | const { password, ...info } = user; 10 | return info; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/usecases/auth/login.usecases.ts: -------------------------------------------------------------------------------- 1 | import { IBcryptService } from '../../domain/adapters/bcrypt.interface'; 2 | import { IJwtService, IJwtServicePayload } from '../../domain/adapters/jwt.interface'; 3 | import { JWTConfig } from '../../domain/config/jwt.interface'; 4 | import { ILogger } from '../../domain/logger/logger.interface'; 5 | import { UserRepository } from '../../domain/repositories/userRepository.interface'; 6 | 7 | export class LoginUseCases { 8 | constructor( 9 | private readonly logger: ILogger, 10 | private readonly jwtTokenService: IJwtService, 11 | private readonly jwtConfig: JWTConfig, 12 | private readonly userRepository: UserRepository, 13 | private readonly bcryptService: IBcryptService, 14 | ) {} 15 | 16 | async getCookieWithJwtToken(username: string) { 17 | this.logger.log('LoginUseCases execute', `The user ${username} have been logged.`); 18 | const payload: IJwtServicePayload = { username: username }; 19 | const secret = this.jwtConfig.getJwtSecret(); 20 | const expiresIn = this.jwtConfig.getJwtExpirationTime() + 's'; 21 | const token = this.jwtTokenService.createToken(payload, secret, expiresIn); 22 | return { token, maxAge: this.jwtConfig.getJwtExpirationTime() }; 23 | } 24 | 25 | async getCookieWithJwtRefreshToken(username: string) { 26 | this.logger.log('LoginUseCases execute', `The user ${username} have been logged.`); 27 | const payload: IJwtServicePayload = { username: username }; 28 | const secret = this.jwtConfig.getJwtRefreshSecret(); 29 | const expiresIn = this.jwtConfig.getJwtRefreshExpirationTime() + 's'; 30 | const token = this.jwtTokenService.createToken(payload, secret, expiresIn); 31 | await this.setCurrentRefreshToken(token, username); 32 | return { 33 | token, 34 | maxAge: this.jwtConfig.getJwtRefreshExpirationTime(), 35 | }; 36 | } 37 | 38 | async validateUserForLocalStragtegy(username: string, pass: string) { 39 | const user = await this.userRepository.getUserByUsername(username); 40 | if (!user) { 41 | return null; 42 | } 43 | const match = await this.bcryptService.compare(pass, user.password); 44 | if (user && match) { 45 | await this.updateLoginTime(user.username); 46 | const { password, ...result } = user; 47 | return result; 48 | } 49 | return null; 50 | } 51 | 52 | async validateUserForJWTStragtegy(username: string) { 53 | const user = await this.userRepository.getUserByUsername(username); 54 | if (!user) { 55 | return null; 56 | } 57 | return user; 58 | } 59 | 60 | async updateLoginTime(username: string) { 61 | await this.userRepository.updateLastLogin(username); 62 | } 63 | 64 | async setCurrentRefreshToken(refreshToken: string, username: string) { 65 | const currentHashedRefreshToken = await this.bcryptService.hash(refreshToken); 66 | await this.userRepository.updateRefreshToken(username, currentHashedRefreshToken); 67 | } 68 | 69 | async getUserIfRefreshTokenMatches(refreshToken: string, username: string) { 70 | const user = await this.userRepository.getUserByUsername(username); 71 | if (!user) { 72 | return null; 73 | } 74 | 75 | const isRefreshTokenMatching = await this.bcryptService.compare(refreshToken, user.hashRefreshToken); 76 | if (isRefreshTokenMatching) { 77 | return user; 78 | } 79 | 80 | return null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/usecases/auth/logout.usecases.ts: -------------------------------------------------------------------------------- 1 | export class LogoutUseCases { 2 | constructor() {} 3 | 4 | async execute(): Promise { 5 | return ['Authentication=; HttpOnly; Path=/; Max-Age=0', 'Refresh=; HttpOnly; Path=/; Max-Age=0']; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/usecases/auth/test/auth.spec.ts: -------------------------------------------------------------------------------- 1 | import { IBcryptService } from '../../../domain/adapters/bcrypt.interface'; 2 | import { IJwtService } from '../../../domain/adapters/jwt.interface'; 3 | import { JWTConfig } from '../../../domain/config/jwt.interface'; 4 | import { IException } from '../../../domain/exceptions/exceptions.interface'; 5 | import { ILogger } from '../../../domain/logger/logger.interface'; 6 | import { UserM } from '../../../domain/model/user'; 7 | import { UserRepository } from '../../../domain/repositories/userRepository.interface'; 8 | import { IsAuthenticatedUseCases } from '../isAuthenticated.usecases'; 9 | import { LoginUseCases } from '../login.usecases'; 10 | import { LogoutUseCases } from '../logout.usecases'; 11 | 12 | describe('uses_cases/authentication', () => { 13 | let loginUseCases: LoginUseCases; 14 | let logoutUseCases: LogoutUseCases; 15 | let isAuthenticated: IsAuthenticatedUseCases; 16 | let logger: ILogger; 17 | let exception: IException; 18 | let jwtService: IJwtService; 19 | let jwtConfig: JWTConfig; 20 | let adminUserRepo: UserRepository; 21 | let bcryptService: IBcryptService; 22 | 23 | beforeEach(() => { 24 | logger = {} as ILogger; 25 | logger.log = jest.fn(); 26 | 27 | exception = {} as IException; 28 | 29 | jwtService = {} as IJwtService; 30 | jwtService.createToken = jest.fn(); 31 | 32 | jwtConfig = {} as JWTConfig; 33 | jwtConfig.getJwtExpirationTime = jest.fn(); 34 | jwtConfig.getJwtSecret = jest.fn(); 35 | jwtConfig.getJwtRefreshSecret = jest.fn(); 36 | jwtConfig.getJwtRefreshExpirationTime = jest.fn(); 37 | 38 | adminUserRepo = {} as UserRepository; 39 | adminUserRepo.getUserByUsername = jest.fn(); 40 | adminUserRepo.updateLastLogin = jest.fn(); 41 | adminUserRepo.updateRefreshToken = jest.fn(); 42 | 43 | bcryptService = {} as IBcryptService; 44 | bcryptService.compare = jest.fn(); 45 | bcryptService.hash = jest.fn(); 46 | 47 | loginUseCases = new LoginUseCases(logger, jwtService, jwtConfig, adminUserRepo, bcryptService); 48 | logoutUseCases = new LogoutUseCases(); 49 | isAuthenticated = new IsAuthenticatedUseCases(adminUserRepo); 50 | }); 51 | 52 | describe('creating a cookie', () => { 53 | it('should return a cookie', async () => { 54 | const expireIn = '200'; 55 | const token = 'token'; 56 | (jwtConfig.getJwtSecret as jest.Mock).mockReturnValue(() => 'secret'); 57 | (jwtConfig.getJwtExpirationTime as jest.Mock).mockReturnValue(expireIn); 58 | (jwtService.createToken as jest.Mock).mockReturnValue(token); 59 | 60 | expect(await loginUseCases.getCookieWithJwtToken('username')).toEqual( 61 | `Authentication=${token}; HttpOnly; Path=/; Max-Age=${expireIn}`, 62 | ); 63 | }); 64 | it('should return a refresh cookie', async () => { 65 | const expireIn = '200'; 66 | const token = 'token'; 67 | (jwtConfig.getJwtRefreshSecret as jest.Mock).mockReturnValue(() => 'secret'); 68 | (jwtConfig.getJwtRefreshExpirationTime as jest.Mock).mockReturnValue(expireIn); 69 | (jwtService.createToken as jest.Mock).mockReturnValue(token); 70 | (bcryptService.hash as jest.Mock).mockReturnValue(Promise.resolve('hashed password')); 71 | (adminUserRepo.updateRefreshToken as jest.Mock).mockReturnValue(Promise.resolve(null)); 72 | 73 | expect(await loginUseCases.getCookieWithJwtRefreshToken('username')).toEqual( 74 | `Refresh=${token}; HttpOnly; Path=/; Max-Age=${expireIn}`, 75 | ); 76 | expect(adminUserRepo.updateRefreshToken).toBeCalledTimes(1); 77 | }); 78 | }); 79 | 80 | describe('validation local strategy', () => { 81 | it('should return null because user not found', async () => { 82 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(null)); 83 | 84 | expect(await loginUseCases.validateUserForLocalStragtegy('username', 'password')).toEqual(null); 85 | }); 86 | it('should return null because wrong password', async () => { 87 | const user: UserM = { 88 | id: 1, 89 | username: 'username', 90 | password: 'password', 91 | createDate: new Date(), 92 | updatedDate: new Date(), 93 | lastLogin: null, 94 | hashRefreshToken: 'refresh token', 95 | }; 96 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(user)); 97 | (bcryptService.compare as jest.Mock).mockReturnValue(Promise.resolve(false)); 98 | 99 | expect(await loginUseCases.validateUserForLocalStragtegy('username', 'password')).toEqual(null); 100 | }); 101 | it('should return user without password', async () => { 102 | const user: UserM = { 103 | id: 1, 104 | username: 'username', 105 | password: 'password', 106 | createDate: new Date(), 107 | updatedDate: new Date(), 108 | lastLogin: null, 109 | hashRefreshToken: 'refresh token', 110 | }; 111 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(user)); 112 | (bcryptService.compare as jest.Mock).mockReturnValue(Promise.resolve(true)); 113 | 114 | const { password, ...rest } = user; 115 | 116 | expect(await loginUseCases.validateUserForLocalStragtegy('username', 'password')).toEqual(rest); 117 | }); 118 | }); 119 | 120 | describe('Validation jwt strategy', () => { 121 | it('should return null because user not found', async () => { 122 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(null)); 123 | (bcryptService.compare as jest.Mock).mockReturnValue(Promise.resolve(false)); 124 | 125 | expect(await loginUseCases.validateUserForJWTStragtegy('username')).toEqual(null); 126 | }); 127 | 128 | it('should return user', async () => { 129 | const user: UserM = { 130 | id: 1, 131 | username: 'username', 132 | password: 'password', 133 | createDate: new Date(), 134 | updatedDate: new Date(), 135 | lastLogin: null, 136 | hashRefreshToken: 'refresh token', 137 | }; 138 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(user)); 139 | 140 | expect(await loginUseCases.validateUserForJWTStragtegy('username')).toEqual(user); 141 | }); 142 | }); 143 | 144 | describe('Validation refresh token', () => { 145 | it('should return null because user not found', async () => { 146 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(null)); 147 | 148 | expect(await loginUseCases.getUserIfRefreshTokenMatches('refresh token', 'username')).toEqual(null); 149 | }); 150 | 151 | it('should return null because user not found', async () => { 152 | const user: UserM = { 153 | id: 1, 154 | username: 'username', 155 | password: 'password', 156 | createDate: new Date(), 157 | updatedDate: new Date(), 158 | lastLogin: null, 159 | hashRefreshToken: 'refresh token', 160 | }; 161 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(user)); 162 | (bcryptService.compare as jest.Mock).mockReturnValue(Promise.resolve(false)); 163 | 164 | expect(await loginUseCases.getUserIfRefreshTokenMatches('refresh token', 'username')).toEqual(null); 165 | }); 166 | 167 | it('should return user', async () => { 168 | const user: UserM = { 169 | id: 1, 170 | username: 'username', 171 | password: 'password', 172 | createDate: new Date(), 173 | updatedDate: new Date(), 174 | lastLogin: null, 175 | hashRefreshToken: 'refresh token', 176 | }; 177 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(user)); 178 | (bcryptService.compare as jest.Mock).mockReturnValue(Promise.resolve(true)); 179 | 180 | expect(await loginUseCases.getUserIfRefreshTokenMatches('refresh token', 'username')).toEqual(user); 181 | }); 182 | }); 183 | 184 | describe('logout', () => { 185 | it('should return an array to invalid the cookie', async () => { 186 | expect(await logoutUseCases.execute()).toEqual([ 187 | 'Authentication=; HttpOnly; Path=/; Max-Age=0', 188 | 'Refresh=; HttpOnly; Path=/; Max-Age=0', 189 | ]); 190 | }); 191 | }); 192 | 193 | describe('isAuthenticated', () => { 194 | it('should return an array to invalid the cookie', async () => { 195 | const user: UserM = { 196 | id: 1, 197 | username: 'username', 198 | password: 'password', 199 | createDate: new Date(), 200 | updatedDate: new Date(), 201 | lastLogin: null, 202 | hashRefreshToken: 'refresh token', 203 | }; 204 | (adminUserRepo.getUserByUsername as jest.Mock).mockReturnValue(Promise.resolve(user)); 205 | 206 | const { password, ...rest } = user; 207 | 208 | expect(await isAuthenticated.execute('username')).toEqual(rest); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /src/usecases/todo/addTodo.usecases.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../../domain/logger/logger.interface'; 2 | import { TodoM } from '../../domain/model/todo'; 3 | import { TodoRepository } from '../../domain/repositories/todoRepository.interface'; 4 | 5 | export class addTodoUseCases { 6 | constructor(private readonly logger: ILogger, private readonly todoRepository: TodoRepository) {} 7 | 8 | async execute(content: string): Promise { 9 | const todo = new TodoM(); 10 | todo.content = content; 11 | todo.isDone = false; 12 | const result = await this.todoRepository.insert(todo); 13 | this.logger.log('addTodoUseCases execute', 'New todo have been inserted'); 14 | return result; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/usecases/todo/deleteTodo.usecases.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../../domain/logger/logger.interface'; 2 | import { TodoRepository } from '../../domain/repositories/todoRepository.interface'; 3 | 4 | export class deleteTodoUseCases { 5 | constructor(private readonly logger: ILogger, private readonly todoRepository: TodoRepository) {} 6 | 7 | async execute(id: number): Promise { 8 | await this.todoRepository.deleteById(id); 9 | this.logger.log('deleteTodoUseCases execute', `Todo ${id} have been deleted`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/usecases/todo/getTodo.usecases.ts: -------------------------------------------------------------------------------- 1 | import { TodoM } from '../../domain/model/todo'; 2 | import { TodoRepository } from '../../domain/repositories/todoRepository.interface'; 3 | 4 | export class GetTodoUseCases { 5 | constructor(private readonly todoRepository: TodoRepository) {} 6 | 7 | async execute(id: number): Promise { 8 | return await this.todoRepository.findById(id); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/usecases/todo/getTodos.usecases.ts: -------------------------------------------------------------------------------- 1 | import { TodoM } from '../../domain/model/todo'; 2 | import { TodoRepository } from '../../domain/repositories/todoRepository.interface'; 3 | 4 | export class getTodosUseCases { 5 | constructor(private readonly todoRepository: TodoRepository) {} 6 | 7 | async execute(): Promise { 8 | return await this.todoRepository.findAll(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/usecases/todo/updateTodo.usecases.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../../domain/logger/logger.interface'; 2 | import { TodoRepository } from '../../domain/repositories/todoRepository.interface'; 3 | 4 | export class updateTodoUseCases { 5 | constructor(private readonly logger: ILogger, private readonly todoRepository: TodoRepository) {} 6 | 7 | async execute(id: number, isDone: boolean): Promise { 8 | await this.todoRepository.updateContent(id, isDone); 9 | this.logger.log('updateTodoUseCases execute', `Todo ${id} have been updated`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { ExecutionContext } from '@nestjs/common'; 3 | import fastifyCookie from '@fastify/cookie'; 4 | import { UseCaseProxy } from '../src/infrastructure/usecases-proxy/usecases-proxy'; 5 | import { UsecasesProxyModule } from '../src/infrastructure/usecases-proxy/usecases-proxy.module'; 6 | import { LoginUseCases } from '../src/usecases/auth/login.usecases'; 7 | import { IsAuthenticatedUseCases } from '../src/usecases/auth/isAuthenticated.usecases'; 8 | import { AppModule } from '../src/app.module'; 9 | import { JwtAuthGuard } from '../src/infrastructure/common/guards/jwtAuth.guard'; 10 | import JwtRefreshGuard from '../src/infrastructure/common/guards/jwtRefresh.guard'; 11 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; 12 | 13 | describe('infrastructure/controllers/auth', () => { 14 | let app: NestFastifyApplication; 15 | let loginUseCase: LoginUseCases; 16 | let isAuthenticatedUseCases: IsAuthenticatedUseCases; 17 | 18 | beforeAll(async () => { 19 | loginUseCase = {} as LoginUseCases; 20 | loginUseCase.getCookieWithJwtToken = jest.fn(); 21 | loginUseCase.validateUserForLocalStragtegy = jest.fn(); 22 | loginUseCase.getCookieWithJwtRefreshToken = jest.fn(); 23 | const loginUsecaseProxyService: UseCaseProxy = { 24 | getInstance: () => loginUseCase, 25 | } as UseCaseProxy; 26 | 27 | isAuthenticatedUseCases = {} as IsAuthenticatedUseCases; 28 | isAuthenticatedUseCases.execute = jest.fn(); 29 | const isAuthUsecaseProxyService: UseCaseProxy = { 30 | getInstance: () => isAuthenticatedUseCases, 31 | } as UseCaseProxy; 32 | 33 | const moduleRef = await Test.createTestingModule({ 34 | imports: [AppModule], 35 | }) 36 | .overrideProvider(UsecasesProxyModule.IS_AUTHENTICATED_USECASES_PROXY) 37 | .useValue(isAuthUsecaseProxyService) 38 | .overrideProvider(UsecasesProxyModule.LOGIN_USECASES_PROXY) 39 | .useValue(loginUsecaseProxyService) 40 | .overrideGuard(JwtAuthGuard) 41 | .useValue({ 42 | canActivate(context: ExecutionContext) { 43 | const req = context.switchToHttp().getRequest(); 44 | req.user = { username: 'username' }; 45 | return req.cookies.accessToken === '123456'; 46 | }, 47 | }) 48 | .overrideGuard(JwtRefreshGuard) 49 | .useValue({ 50 | canActivate(context: ExecutionContext) { 51 | const req = context.switchToHttp().getRequest(); 52 | req.user = { username: 'username' }; 53 | return true; 54 | }, 55 | }) 56 | .compile(); 57 | 58 | app = moduleRef.createNestApplication(new FastifyAdapter()); 59 | await app.register(fastifyCookie); 60 | await app.init(); 61 | await app.getHttpAdapter().getInstance().ready(); 62 | }); 63 | 64 | it(`/POST login should return 201`, async () => { 65 | const createDate = new Date().toISOString(); 66 | const updatedDate = new Date().toISOString(); 67 | (loginUseCase.validateUserForLocalStragtegy as jest.Mock).mockReturnValue( 68 | Promise.resolve({ 69 | id: 1, 70 | username: 'username', 71 | createDate: createDate, 72 | updatedDate: updatedDate, 73 | lastLogin: null, 74 | hashRefreshToken: null, 75 | }), 76 | ); 77 | (loginUseCase.getCookieWithJwtToken as jest.Mock).mockReturnValue(Promise.resolve({ token: '123456', maxAge: '1800' })); 78 | (loginUseCase.getCookieWithJwtRefreshToken as jest.Mock).mockReturnValue( 79 | Promise.resolve({ token: '12345', maxAge: '86400' }), 80 | ); 81 | 82 | await app 83 | .inject({ 84 | method: 'POST', 85 | url: '/auth/login', 86 | payload: { username: 'username', password: 'password' }, 87 | }) 88 | .then((result) => { 89 | expect(result.statusCode).toEqual(201); 90 | expect(result.headers['set-cookie']).toEqual([ 91 | `accessToken=123456; Max-Age=1800; Path=/; HttpOnly; Secure`, 92 | `refreshToken=12345; Max-Age=86400; Path=/; HttpOnly; Secure`, 93 | ]); 94 | }); 95 | }); 96 | 97 | it(`/POST logout should return 201`, async () => { 98 | app 99 | .inject({ 100 | method: 'POST', 101 | cookies: { accessToken: '123456' }, 102 | url: '/auth/logout', 103 | payload: { username: 'username', password: 'password' }, 104 | }) 105 | .then((result) => { 106 | expect(result.statusCode).toEqual(201); 107 | expect(result.headers['set-cookie']).toEqual([ 108 | 'accessToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT', 109 | 'refreshToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT', 110 | ]); 111 | }); 112 | }); 113 | 114 | it(`/POST login should return 401`, async () => { 115 | (loginUseCase.validateUserForLocalStragtegy as jest.Mock).mockReturnValue(Promise.resolve(null)); 116 | 117 | await app 118 | .inject({ 119 | method: 'POST', 120 | url: '/auth/login', 121 | payload: { username: 'username', password: 'password' }, 122 | }) 123 | .then((result) => { 124 | expect(result.statusCode).toEqual(401); 125 | }); 126 | }); 127 | 128 | it(`/POST Refresh token should return 201`, async () => { 129 | (loginUseCase.getCookieWithJwtToken as jest.Mock).mockReturnValue({ 130 | token: '123456', 131 | maxAge: process.env.JWT_EXPIRATION_TIME, 132 | }); 133 | 134 | app 135 | .inject({ 136 | method: 'GET', 137 | url: '/auth/refresh', 138 | }) 139 | .then((result) => { 140 | expect(result.statusCode).toEqual(200); 141 | expect(result.headers['set-cookie']).toEqual('accessToken=123456; Max-Age=1800; Path=/; HttpOnly; Secure'); 142 | }); 143 | }); 144 | 145 | it(`/GET is_authenticated should return 200`, async () => { 146 | (isAuthenticatedUseCases.execute as jest.Mock).mockReturnValue(Promise.resolve({ username: 'username' })); 147 | 148 | app 149 | .inject({ 150 | method: 'GET', 151 | url: '/auth/is_authenticated', 152 | cookies: { accessToken: '123456' }, 153 | }) 154 | .then((result) => { 155 | expect(result.statusCode).toEqual(200); 156 | }); 157 | }); 158 | 159 | it(`/GET is_authenticated should return 403`, async () => { 160 | (isAuthenticatedUseCases.execute as jest.Mock).mockReturnValue(Promise.resolve({ username: 'username' })); 161 | 162 | app 163 | .inject({ 164 | method: 'GET', 165 | url: '/auth/is_authenticated', 166 | }) 167 | .then((result) => { 168 | expect(result.statusCode).toEqual(403); 169 | }); 170 | }); 171 | 172 | afterAll(async () => { 173 | await app.close(); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /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": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------