├── .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 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------