├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .github └── banner-auth-nestjs.png ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── docker-compose.yml ├── nest-cli.json ├── package.json ├── src ├── app.module.ts ├── database │ ├── config │ │ └── typeorm.config.ts │ └── migrations │ │ └── 1601301473024-CreateUsersTable.ts ├── main.ts ├── modules │ ├── auth │ │ ├── auth.module.ts │ │ ├── controller │ │ │ ├── auth.controller.spec.ts │ │ │ └── auth.controller.ts │ │ ├── guards │ │ │ ├── jwt-auth.guard.ts │ │ │ └── local-auth.guard.ts │ │ ├── service │ │ │ ├── auth.service.spec.ts │ │ │ └── auth.service.ts │ │ └── strategies │ │ │ ├── jwt.strategy.ts │ │ │ └── local.strategy.ts │ └── users │ │ ├── controller │ │ ├── users.controller.spec.ts │ │ └── users.controller.ts │ │ ├── dtos │ │ ├── create-user.dto.ts │ │ └── update-user.dto.ts │ │ ├── entity │ │ └── users.entity.ts │ │ ├── service │ │ ├── users.service.spec.ts │ │ └── users.service.ts │ │ └── users.module.ts └── shared │ ├── exception-filters │ └── duplicate-key.exception-filter.ts │ ├── services │ └── crypto.service.ts │ └── swagger │ └── responses │ ├── auth.response.ts │ ├── error.response.ts │ └── users.response.ts ├── test ├── jest-e2e.json └── utils │ └── test.uttil.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_HOST=localhost 2 | DATABASE_PORT=5432 3 | DATABASE_USERNAME= # usuário do banco 4 | DATABASE_PASSWORD= # senha do banco 5 | DATABASE_NAME= # nome do banco 6 | DATBASE_SYNC=true 7 | 8 | JWT_SECRET_KEY= # chave secreta do JWT 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/banner-auth-nestjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goncadanilo/auth-nestjs/da6542f11d67f0a8b6f146d0df303613d34aac4a/.github/banner-auth-nestjs.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # Environment 37 | .env 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Danilo Gonçalves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 🔐 Auth NestJS - JWT 3 |

4 | 5 |

6 | GitHub language count 7 | 8 | 9 | GitHub last commit 10 | 11 | 12 | 13 | Repository issues 14 | 15 | 16 | License 17 | 18 | 19 | Author 20 | 21 |

22 | 23 |

24 | Tecnologias   |    25 | Projeto   |    26 | Como rodar   |    27 | Licença 28 |

29 | 30 |
31 | 32 |

33 | Auth NestJS 34 |

35 | 36 | ## 🚀 Tecnologias 37 | 38 | Esse projeto foi desenvolvido com as seguintes tecnologias: 39 | 40 | - [NestJS](https://nestjs.com/): framework utilizado para criação da aplicação. 41 | - [JWT](https://jwt.io/): utilizado para gerar o token de autenticação. 42 | - [Postgres](https://www.postgresql.org/): banco SQL utilizado para armazenar os dados. 43 | - [Docker](https://www.docker.com/) e [Docker-compose](https://docs.docker.com/compose/install/): utilizado para criar e rodar o container do banco de dados. 44 | - [Jest](https://jestjs.io/): utilizado para escrever os testes da aplicação. 45 | - [Swagger](https://swagger.io/): utilizado para documentar a aplicação. 46 | 47 | ## 💻 Projeto 48 | 49 | Esse projeto é um sistema de autenticação desenvolvido para fins de estudo utilizando o framework [NestJS](https://nestjs.com/). A aplicação consiste no cadastro, autenticação e atualização do usuário. Lembrando que para conseguir atulizar os seus dados, o usuário deve estar autenticado. 50 | 51 | ## ⚡ Como rodar 52 | 53 | ### Requisitos 54 | 55 | - [Node.js](https://nodejs.org/en/). 56 | - [NestJS CLI](https://docs.nestjs.com/first-steps). 57 | - [Yarn](https://yarnpkg.com/) ou se preferir, pode usar o npm _(já vem com o node)_. 58 | - [Docker](https://www.docker.com/) e [Docker-compose](https://docs.docker.com/compose/install/) _(opcional)_. 59 | 60 | ### Subir o banco 61 | 62 | - crie uma cópia do `.env.example` como `.env` e defina suas variáveis do banco. 63 | - suba o banco de dados com docker: `docker-compose up -d`. 64 | 65 | _(se você não estiver usando o docker, é necessário criar o banco manualmente)_. 66 | 67 | - rode as migrations: `yarn typeorm migration:run`. 68 | 69 | ### Rodar a aplicação 70 | 71 | - para rodar a aplicação: `yarn start`. 72 | - para rodar a aplicação em modo watch: `yarn start:dev`. 73 | - a aplicação estará disponível no endereço: `http://localhost:3000`. 74 | - a documentação estará disponível no endereço: `http://localhost:3000/api`. 75 | 76 | ### Rodar os testes 77 | 78 | - para rodar os testes unitários: `yarn test`. 79 | - para ver a cobertura dos testes unitários: `yarn test:cov`. 80 | - para rodar os testes e2e: `yarn test:e2e` 81 | 82 | ## 📝 Licença 83 | 84 | Esse projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes. 85 | 86 | --- 87 | 88 | Feito com ♥ by [Danilo Gonçalves](https://github.com/goncadanilo). Me adicione no [LinkedIn](https://www.linkedin.com/in/goncadanilo/) :wave: 89 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | container_name: auth_nestjs_db 6 | image: postgres:9.6-alpine 7 | environment: 8 | POSTGRES_PASSWORD: ${DATABASE_PASSWORD} 9 | POSTGRES_USER: ${DATABASE_USERNAME} 10 | POSTGRES_DB: ${DATABASE_NAME} 11 | PG_DATA: /var/lib/postgresql/data 12 | ports: 13 | - ${DATABASE_PORT}:5432 14 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-nestjs", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json", 22 | "typeorm": "ts-node ./node_modules/typeorm/cli.js --config src/database/config/typeorm.config.ts" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^7.0.0", 26 | "@nestjs/config": "^0.5.0", 27 | "@nestjs/core": "^7.0.0", 28 | "@nestjs/jwt": "^7.1.0", 29 | "@nestjs/passport": "^7.1.0", 30 | "@nestjs/platform-express": "^7.0.0", 31 | "@nestjs/swagger": "^4.6.1", 32 | "@nestjs/typeorm": "^7.1.4", 33 | "bcryptjs": "^2.4.3", 34 | "class-transformer": "^0.3.1", 35 | "class-validator": "^0.12.2", 36 | "passport": "^0.4.1", 37 | "passport-jwt": "^4.0.0", 38 | "passport-local": "^1.0.0", 39 | "pg": "^8.3.3", 40 | "reflect-metadata": "^0.1.13", 41 | "rimraf": "^3.0.2", 42 | "rxjs": "^6.5.4", 43 | "swagger-ui-express": "^4.1.4", 44 | "typeorm": "^0.2.26" 45 | }, 46 | "devDependencies": { 47 | "@nestjs/cli": "^7.0.0", 48 | "@nestjs/schematics": "^7.0.0", 49 | "@nestjs/testing": "^7.0.0", 50 | "@types/bcryptjs": "^2.4.2", 51 | "@types/express": "^4.17.3", 52 | "@types/jest": "26.0.10", 53 | "@types/node": "^13.9.1", 54 | "@types/passport-jwt": "^3.0.3", 55 | "@types/passport-local": "^1.0.33", 56 | "@types/supertest": "^2.0.8", 57 | "@typescript-eslint/eslint-plugin": "3.9.1", 58 | "@typescript-eslint/parser": "3.9.1", 59 | "eslint": "7.7.0", 60 | "eslint-config-prettier": "^6.10.0", 61 | "eslint-plugin-import": "^2.20.1", 62 | "jest": "26.4.2", 63 | "prettier": "^1.19.1", 64 | "supertest": "^4.0.2", 65 | "ts-jest": "26.2.0", 66 | "ts-loader": "^6.2.1", 67 | "ts-node": "9.0.0", 68 | "tsconfig-paths": "^3.9.0", 69 | "typescript": "^3.7.4" 70 | }, 71 | "jest": { 72 | "moduleFileExtensions": [ 73 | "js", 74 | "json", 75 | "ts" 76 | ], 77 | "rootDir": "src", 78 | "testRegex": ".spec.ts$", 79 | "transform": { 80 | "^.+\\.(t|j)s$": "ts-jest" 81 | }, 82 | "coverageDirectory": "../coverage", 83 | "testEnvironment": "node" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { resolve } from 'path'; 5 | import { UsersModule } from './modules/users/users.module'; 6 | import { AuthModule } from './modules/auth/auth.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot(), 11 | TypeOrmModule.forRoot({ 12 | type: 'postgres', 13 | host: process.env.DATABASE_HOST, 14 | port: +process.env.DATABASE_PORT, 15 | username: process.env.DATABASE_USERNAME, 16 | password: process.env.DATABASE_PASSWORD, 17 | database: process.env.DATABASE_NAME, 18 | entities: [ 19 | resolve(__dirname, 'modules', '**', 'entity', '*.entity.{ts,js}'), 20 | ], 21 | synchronize: process.env.DATABASE_SYNC === 'true', 22 | }), 23 | UsersModule, 24 | AuthModule, 25 | ], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /src/database/config/typeorm.config.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | import { join, resolve } from 'path'; 3 | 4 | const options: TypeOrmModuleOptions = { 5 | type: 'postgres', 6 | host: process.env.DATABASE_HOST, 7 | port: +process.env.DATABASE_PORT, 8 | username: process.env.DATABASE_USERNAME, 9 | password: process.env.DATABASE_PASSWORD, 10 | database: process.env.DATABASE_NAME, 11 | entities: [ 12 | resolve( 13 | __dirname, 14 | '..', 15 | '..', 16 | 'modules', 17 | '**', 18 | 'entity', 19 | '*.entity.{ts,js}', 20 | ), 21 | ], 22 | migrations: [resolve(__dirname, '..', 'migrations', '*.{ts,js}')], 23 | cli: { 24 | migrationsDir: join('src', 'database', 'migrations'), 25 | }, 26 | }; 27 | 28 | module.exports = options; 29 | -------------------------------------------------------------------------------- /src/database/migrations/1601301473024-CreateUsersTable.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | 3 | export class CreateUsersTable1601301473024 implements MigrationInterface { 4 | private table = new Table({ 5 | name: 'users', 6 | columns: [ 7 | { 8 | name: 'id', 9 | type: 'int', 10 | isPrimary: true, 11 | isGenerated: true, 12 | generationStrategy: 'increment', 13 | }, 14 | { 15 | name: 'name', 16 | type: 'varchar', 17 | isNullable: false, 18 | }, 19 | { 20 | name: 'email', 21 | type: 'varchar', 22 | isNullable: false, 23 | isUnique: true, 24 | }, 25 | { 26 | name: 'password', 27 | type: 'varchar', 28 | isNullable: false, 29 | }, 30 | { 31 | name: 'created_at', 32 | type: 'timestamp', 33 | default: 'now()', 34 | }, 35 | ], 36 | }); 37 | 38 | public async up(queryRunner: QueryRunner): Promise { 39 | await queryRunner.createTable(this.table); 40 | } 41 | 42 | public async down(queryRunner: QueryRunner): Promise { 43 | await queryRunner.dropTable(this.table); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | const options = new DocumentBuilder() 9 | .setTitle('Auth NestJS API') 10 | .setDescription('Auth NestJS API Documentation') 11 | .setVersion('1.0') 12 | .build(); 13 | 14 | const document = SwaggerModule.createDocument(app, options); 15 | SwaggerModule.setup('api', app, document); 16 | 17 | await app.listen(3000); 18 | } 19 | bootstrap(); 20 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { CryptoService } from '../../shared/services/crypto.service'; 6 | import { UsersModule } from '../users/users.module'; 7 | import { AuthController } from './controller/auth.controller'; 8 | import { AuthService } from './service/auth.service'; 9 | import { JwtStrategy } from './strategies/jwt.strategy'; 10 | import { LocalStrategy } from './strategies/local.strategy'; 11 | 12 | @Module({ 13 | imports: [ 14 | UsersModule, 15 | PassportModule, 16 | ConfigModule.forRoot(), 17 | JwtModule.register({ 18 | secret: process.env.JWT_SECRET_KEY, 19 | signOptions: { expiresIn: '1d' }, 20 | }), 21 | ], 22 | providers: [AuthService, LocalStrategy, JwtStrategy, CryptoService], 23 | controllers: [AuthController], 24 | }) 25 | export class AuthModule {} 26 | -------------------------------------------------------------------------------- /src/modules/auth/controller/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { TestUtil } from '../../../../test/utils/test.uttil'; 3 | import { Users } from '../../users/entity/users.entity'; 4 | import { AuthService } from '../service/auth.service'; 5 | import { AuthController } from './auth.controller'; 6 | 7 | describe('AuthController', () => { 8 | let authController: AuthController; 9 | let mockUser: Users; 10 | 11 | const mockAuthService = { 12 | login: jest.fn(), 13 | }; 14 | 15 | beforeAll(async () => { 16 | const moduleRef = await Test.createTestingModule({ 17 | controllers: [AuthController], 18 | providers: [{ provide: AuthService, useValue: mockAuthService }], 19 | }).compile(); 20 | 21 | authController = moduleRef.get(AuthController); 22 | mockUser = TestUtil.getMockUser(); 23 | }); 24 | 25 | beforeEach(() => { 26 | mockAuthService.login.mockReset(); 27 | }); 28 | 29 | it('should be defined', () => { 30 | expect(authController).toBeDefined(); 31 | }); 32 | 33 | describe('when login', () => { 34 | it('should authentication user and return an authentication token', async () => { 35 | mockAuthService.login.mockReturnValue({ token: 'valid_token' }); 36 | 37 | const user = { 38 | id: mockUser.id, 39 | email: mockUser.email, 40 | }; 41 | 42 | const result = await authController.login(user); 43 | 44 | expect(result).toHaveProperty('token', 'valid_token'); 45 | expect(mockAuthService.login).toBeCalledTimes(1); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/modules/auth/controller/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | HttpCode, 4 | HttpStatus, 5 | Post, 6 | Request, 7 | UseGuards, 8 | } from '@nestjs/common'; 9 | import { 10 | ApiOkResponse, 11 | ApiOperation, 12 | ApiTags, 13 | ApiUnauthorizedResponse, 14 | } from '@nestjs/swagger'; 15 | import { AuthResponse } from '../../../shared/swagger/responses/auth.response'; 16 | import { ErrorResponse } from '../../../shared/swagger/responses/error.response'; 17 | import { LocalAuthGuard } from '../guards/local-auth.guard'; 18 | import { AuthService } from '../service/auth.service'; 19 | 20 | @ApiTags('Auth') 21 | @Controller('auth') 22 | export class AuthController { 23 | constructor(private authService: AuthService) {} 24 | 25 | @Post() 26 | @HttpCode(HttpStatus.OK) 27 | @UseGuards(LocalAuthGuard) 28 | @ApiOperation({ summary: 'User authentication' }) 29 | @ApiOkResponse({ type: AuthResponse, description: 'Authentication token' }) 30 | @ApiUnauthorizedResponse({ type: ErrorResponse, description: 'Unauthorized' }) 31 | async login(@Request() req: any) { 32 | return this.authService.login(req.user); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/auth/guards/jwt-auth.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/modules/auth/guards/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/modules/auth/service/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtService } from '@nestjs/jwt'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { TestUtil } from '../../../../test/utils/test.uttil'; 4 | import { CryptoService } from '../../../shared/services/crypto.service'; 5 | import { Users } from '../../users/entity/users.entity'; 6 | import { UsersService } from '../../users/service/users.service'; 7 | import { AuthService } from './auth.service'; 8 | 9 | describe('AuthService', () => { 10 | let authService: AuthService; 11 | let mockUser: Users; 12 | 13 | const mockUsersService = { 14 | findUserByEmail: jest.fn(), 15 | }; 16 | 17 | const mockCryptoService = { 18 | compareHash: jest.fn(), 19 | }; 20 | 21 | const mockJwtService = { 22 | sign: jest.fn(), 23 | }; 24 | 25 | beforeAll(async () => { 26 | const moduleRef: TestingModule = await Test.createTestingModule({ 27 | providers: [ 28 | AuthService, 29 | { provide: UsersService, useValue: mockUsersService }, 30 | { provide: CryptoService, useValue: mockCryptoService }, 31 | { provide: JwtService, useValue: mockJwtService }, 32 | ], 33 | }).compile(); 34 | 35 | authService = moduleRef.get(AuthService); 36 | mockUser = TestUtil.getMockUser(); 37 | }); 38 | 39 | beforeEach(() => { 40 | mockUsersService.findUserByEmail.mockReset(); 41 | mockCryptoService.compareHash.mockReset(); 42 | mockJwtService.sign.mockReset(); 43 | }); 44 | 45 | it('should be defined', () => { 46 | expect(authService).toBeDefined(); 47 | }); 48 | 49 | describe('when validate user', () => { 50 | it('should validate user', async () => { 51 | mockUsersService.findUserByEmail.mockReturnValue(mockUser); 52 | mockCryptoService.compareHash.mockReturnValue(true); 53 | 54 | const result = await authService.validateUser( 55 | mockUser.email, 56 | mockUser.password, 57 | ); 58 | 59 | expect(result).toHaveProperty('id', 1); 60 | expect(mockUsersService.findUserByEmail).toBeCalledWith(mockUser.email); 61 | expect(mockUsersService.findUserByEmail).toBeCalledTimes(1); 62 | expect(mockCryptoService.compareHash).toBeCalledWith( 63 | mockUser.password, 64 | mockUser.password, 65 | ); 66 | expect(mockCryptoService.compareHash).toBeCalledTimes(1); 67 | }); 68 | 69 | it('should retutn null if the user is invalid', async () => { 70 | mockUsersService.findUserByEmail.mockReturnValue(null); 71 | mockCryptoService.compareHash.mockReturnValue(false); 72 | 73 | const result = await authService.validateUser( 74 | mockUser.email, 75 | mockUser.password, 76 | ); 77 | 78 | expect(result).toBe(null); 79 | expect(mockUsersService.findUserByEmail).toBeCalledWith(mockUser.email); 80 | expect(mockUsersService.findUserByEmail).toBeCalledTimes(1); 81 | expect(mockCryptoService.compareHash).toBeCalledTimes(0); 82 | }); 83 | }); 84 | 85 | describe('when login', () => { 86 | it('should return an authentication token', async () => { 87 | mockJwtService.sign.mockReturnValue('valid_token'); 88 | 89 | const user = { 90 | id: mockUser.id, 91 | email: mockUser.email, 92 | }; 93 | 94 | const result = await authService.login(user); 95 | 96 | expect(result).toHaveProperty('token', 'valid_token'); 97 | expect(mockJwtService.sign).toBeCalledWith({ 98 | sub: user.id, 99 | email: user.email, 100 | }); 101 | expect(mockJwtService.sign).toBeCalledTimes(1); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/modules/auth/service/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { CryptoService } from '../../../shared/services/crypto.service'; 4 | import { UsersService } from '../../users/service/users.service'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | constructor( 9 | private usersService: UsersService, 10 | private cryptoService: CryptoService, 11 | private jwtService: JwtService, 12 | ) {} 13 | 14 | async validateUser(email: string, password: string) { 15 | const user = await this.usersService.findUserByEmail(email); 16 | const correctPassword = 17 | user && (await this.cryptoService.compareHash(password, user.password)); 18 | 19 | if (correctPassword) { 20 | return { id: user.id, email: user.email }; 21 | } 22 | 23 | return null; 24 | } 25 | 26 | async login(user: any) { 27 | const payload = { sub: user.id, email: user.email }; 28 | 29 | return { 30 | token: this.jwtService.sign(payload), 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | 5 | @Injectable() 6 | export class JwtStrategy extends PassportStrategy(Strategy) { 7 | constructor() { 8 | super({ 9 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 10 | ignoreExpiration: false, 11 | secretOrKey: process.env.JWT_SECRET_KEY, 12 | }); 13 | } 14 | 15 | async validate(payload: any) { 16 | return { id: payload.sub, email: payload.email }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Strategy } from 'passport-local'; 4 | import { AuthService } from '../service/auth.service'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private authService: AuthService) { 9 | super({ 10 | usernameField: 'email', 11 | passwordField: 'password', 12 | }); 13 | } 14 | 15 | async validate(email: string, password: string): Promise { 16 | const user = await this.authService.validateUser(email, password); 17 | 18 | if (!user) { 19 | throw new UnauthorizedException('Incorrect email or password'); 20 | } 21 | 22 | return user; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/users/controller/users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TestUtil } from '../../../../test/utils/test.uttil'; 3 | import { Users } from '../entity/users.entity'; 4 | import { UsersService } from '../service/users.service'; 5 | import { UsersController } from './users.controller'; 6 | 7 | describe('UsersController', () => { 8 | let usersController: UsersController; 9 | let mockUser: Users; 10 | 11 | const mockUsersService = { 12 | createUser: jest.fn(), 13 | updateUser: jest.fn(), 14 | }; 15 | 16 | beforeAll(async () => { 17 | const moduleRef: TestingModule = await Test.createTestingModule({ 18 | controllers: [UsersController], 19 | providers: [{ provide: UsersService, useValue: mockUsersService }], 20 | }).compile(); 21 | 22 | usersController = moduleRef.get(UsersController); 23 | mockUser = TestUtil.getMockUser(); 24 | }); 25 | 26 | beforeEach(() => { 27 | mockUsersService.createUser.mockReset(); 28 | mockUsersService.updateUser.mockReset(); 29 | }); 30 | 31 | it('should be defined', () => { 32 | expect(usersController).toBeDefined(); 33 | }); 34 | 35 | describe('when create a user', () => { 36 | it('should create a user and return it', async () => { 37 | mockUsersService.createUser.mockReturnValue(mockUser); 38 | 39 | const user = { 40 | name: mockUser.name, 41 | email: mockUser.email, 42 | password: mockUser.password, 43 | }; 44 | 45 | const createdUser = await usersController.createUser(user); 46 | 47 | expect(createdUser).toMatchObject(mockUser); 48 | expect(mockUsersService.createUser).toBeCalledWith(user); 49 | expect(mockUsersService.createUser).toBeCalledTimes(1); 50 | }); 51 | }); 52 | 53 | describe('when update a user', () => { 54 | it('should update a existing user and return it', async () => { 55 | const userEmailUpdate = { 56 | email: 'update@email.com', 57 | }; 58 | 59 | const req = { 60 | user: { 61 | id: '1', 62 | }, 63 | }; 64 | 65 | mockUsersService.updateUser.mockReturnValue({ 66 | ...mockUser, 67 | ...userEmailUpdate, 68 | }); 69 | 70 | const updatedProduct = await usersController.updateUser( 71 | req, 72 | userEmailUpdate, 73 | ); 74 | 75 | expect(updatedProduct).toMatchObject(userEmailUpdate); 76 | expect(mockUsersService.updateUser).toBeCalledWith('1', userEmailUpdate); 77 | expect(mockUsersService.updateUser).toBeCalledTimes(1); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/modules/users/controller/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | ClassSerializerInterceptor, 4 | Controller, 5 | HttpCode, 6 | HttpStatus, 7 | Post, 8 | Put, 9 | Request, 10 | UseFilters, 11 | UseGuards, 12 | UseInterceptors, 13 | } from '@nestjs/common'; 14 | import { 15 | ApiBadRequestResponse, 16 | ApiCreatedResponse, 17 | ApiNotFoundResponse, 18 | ApiOkResponse, 19 | ApiOperation, 20 | ApiTags, 21 | } from '@nestjs/swagger'; 22 | import { DuplicateKeyExceptionFilter } from '../../../shared/exception-filters/duplicate-key.exception-filter'; 23 | import { ErrorResponse } from '../../../shared/swagger/responses/error.response'; 24 | import { UsersResponse } from '../../../shared/swagger/responses/users.response'; 25 | import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; 26 | import { CreateUserDto } from '../dtos/create-user.dto'; 27 | import { UpdateUserDto } from '../dtos/update-user.dto'; 28 | import { Users } from '../entity/users.entity'; 29 | import { UsersService } from '../service/users.service'; 30 | 31 | @ApiTags('Users') 32 | @Controller('users') 33 | export class UsersController { 34 | constructor(private usersService: UsersService) {} 35 | 36 | @Post() 37 | @HttpCode(HttpStatus.CREATED) 38 | @UseInterceptors(ClassSerializerInterceptor) 39 | @UseFilters(DuplicateKeyExceptionFilter) 40 | @ApiOperation({ summary: 'Create a new user' }) 41 | @ApiCreatedResponse({ type: UsersResponse, description: 'Created user' }) 42 | @ApiBadRequestResponse({ type: ErrorResponse, description: 'Bad Request' }) 43 | async createUser(@Body() data: CreateUserDto): Promise { 44 | return this.usersService.createUser(data); 45 | } 46 | 47 | @Put() 48 | @HttpCode(HttpStatus.OK) 49 | @UseGuards(JwtAuthGuard) 50 | @UseInterceptors(ClassSerializerInterceptor) 51 | @ApiOperation({ summary: 'Update a user' }) 52 | @ApiOkResponse({ type: UsersResponse, description: 'Updated user' }) 53 | @ApiNotFoundResponse({ type: ErrorResponse, description: 'Not Found' }) 54 | async updateUser( 55 | @Request() req: any, 56 | @Body() data: UpdateUserDto, 57 | ): Promise { 58 | return this.usersService.updateUser(req.user.id, data); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/users/dtos/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 3 | 4 | export class CreateUserDto { 5 | @ApiProperty() 6 | @IsString() 7 | @IsNotEmpty() 8 | name: string; 9 | 10 | @ApiProperty() 11 | @IsEmail() 12 | @IsNotEmpty() 13 | email: string; 14 | 15 | @ApiProperty() 16 | @IsString() 17 | @IsNotEmpty() 18 | password: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/users/dtos/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 3 | 4 | export class UpdateUserDto { 5 | @ApiProperty({ required: false }) 6 | @IsString() 7 | @IsNotEmpty() 8 | name?: string; 9 | 10 | @ApiProperty({ required: false }) 11 | @IsEmail() 12 | @IsNotEmpty() 13 | email?: string; 14 | 15 | @ApiProperty({ required: false }) 16 | @IsString() 17 | @IsNotEmpty() 18 | password?: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/users/entity/users.entity.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | } from 'typeorm'; 8 | 9 | @Entity() 10 | export class Users { 11 | @PrimaryGeneratedColumn() 12 | id: number; 13 | 14 | @Column() 15 | name: string; 16 | 17 | @Column({ unique: true }) 18 | email: string; 19 | 20 | @Exclude() 21 | @Column() 22 | password: string; 23 | 24 | @CreateDateColumn({ name: 'created_at' }) 25 | createdAt: Date; 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/users/service/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { TestUtil } from '../../../../test/utils/test.uttil'; 5 | import { CryptoService } from '../../../shared/services/crypto.service'; 6 | import { Users } from '../entity/users.entity'; 7 | import { UsersService } from './users.service'; 8 | 9 | describe('UsersService', () => { 10 | let usersService: UsersService; 11 | let mockUser: Users; 12 | 13 | const mockRepository = { 14 | create: jest.fn(), 15 | save: jest.fn(), 16 | findOne: jest.fn(), 17 | update: jest.fn(), 18 | }; 19 | 20 | const mockCryptoService = { 21 | generateHash: jest.fn(), 22 | }; 23 | 24 | beforeAll(async () => { 25 | const moduleRef: TestingModule = await Test.createTestingModule({ 26 | providers: [ 27 | UsersService, 28 | { provide: getRepositoryToken(Users), useValue: mockRepository }, 29 | { provide: CryptoService, useValue: mockCryptoService }, 30 | ], 31 | }).compile(); 32 | 33 | usersService = moduleRef.get(UsersService); 34 | mockUser = TestUtil.getMockUser(); 35 | }); 36 | 37 | beforeEach(() => { 38 | mockRepository.create.mockReset(); 39 | mockRepository.save.mockReset(); 40 | mockRepository.findOne.mockReset(); 41 | mockRepository.update.mockReset(); 42 | mockCryptoService.generateHash.mockReset(); 43 | }); 44 | 45 | it('should be defined', () => { 46 | expect(usersService).toBeDefined(); 47 | }); 48 | 49 | describe('when create a user', () => { 50 | it('should be create a user', async () => { 51 | mockRepository.create.mockReturnValue(mockUser); 52 | mockRepository.save.mockReturnValue(mockUser); 53 | mockCryptoService.generateHash.mockReturnValue(mockUser.password); 54 | 55 | const user = { 56 | name: mockUser.name, 57 | email: mockUser.email, 58 | password: mockUser.password, 59 | }; 60 | 61 | const savedUser = await usersService.createUser(user); 62 | 63 | expect(savedUser).toHaveProperty('id', 1); 64 | expect(savedUser).toMatchObject(mockUser); 65 | expect(mockRepository.create).toBeCalledWith(user); 66 | expect(mockRepository.create).toBeCalledTimes(1); 67 | expect(mockCryptoService.generateHash).toBeCalledWith(mockUser.password); 68 | expect(mockCryptoService.generateHash).toBeCalledTimes(1); 69 | expect(mockRepository.save).toBeCalledWith(mockUser); 70 | expect(mockRepository.save).toBeCalledTimes(1); 71 | }); 72 | }); 73 | 74 | describe('when search a user by email', () => { 75 | it('should find a user by email', async () => { 76 | mockRepository.findOne.mockReturnValue(mockUser); 77 | 78 | const userFound = await usersService.findUserByEmail(mockUser.email); 79 | 80 | expect(userFound).toMatchObject(mockUser); 81 | expect(mockRepository.findOne).toBeCalledWith({ email: mockUser.email }); 82 | expect(mockRepository.findOne).toBeCalledTimes(1); 83 | }); 84 | }); 85 | 86 | describe('when update a user', () => { 87 | it('should update a existing user', async () => { 88 | const userEmailUpdate = { 89 | email: 'update@email.com', 90 | }; 91 | 92 | mockRepository.findOne.mockReturnValue(mockUser); 93 | mockRepository.update.mockReturnValue({ 94 | ...mockUser, 95 | ...userEmailUpdate, 96 | }); 97 | mockRepository.create.mockReturnValue({ 98 | ...mockUser, 99 | ...userEmailUpdate, 100 | }); 101 | 102 | const updatedProduct = await usersService.updateUser( 103 | '1', 104 | userEmailUpdate, 105 | ); 106 | 107 | expect(updatedProduct).toMatchObject(userEmailUpdate); 108 | expect(mockRepository.findOne).toBeCalledWith('1'); 109 | expect(mockRepository.findOne).toBeCalledTimes(1); 110 | expect(mockRepository.update).toBeCalledWith('1', userEmailUpdate); 111 | expect(mockRepository.update).toBeCalledTimes(1); 112 | expect(mockRepository.create).toBeCalledWith({ 113 | ...mockUser, 114 | ...userEmailUpdate, 115 | }); 116 | expect(mockRepository.create).toBeCalledTimes(1); 117 | }); 118 | 119 | it('should return a exception when does not to find a user', async () => { 120 | mockRepository.findOne.mockReturnValue(null); 121 | 122 | const userEmailUpdate = { 123 | email: 'update@email.com', 124 | }; 125 | 126 | await usersService.updateUser('3', userEmailUpdate).catch(error => { 127 | expect(error).toBeInstanceOf(NotFoundException); 128 | expect(error).toMatchObject({ message: 'User not found' }); 129 | expect(mockRepository.findOne).toBeCalledWith('3'); 130 | expect(mockRepository.findOne).toBeCalledTimes(1); 131 | }); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/modules/users/service/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { CryptoService } from '../../../shared/services/crypto.service'; 5 | import { CreateUserDto } from '../dtos/create-user.dto'; 6 | import { UpdateUserDto } from '../dtos/update-user.dto'; 7 | import { Users } from '../entity/users.entity'; 8 | 9 | @Injectable() 10 | export class UsersService { 11 | constructor( 12 | @InjectRepository(Users) 13 | private repository: Repository, 14 | private cryptoService: CryptoService, 15 | ) {} 16 | 17 | async createUser(data: CreateUserDto): Promise { 18 | const user = this.repository.create(data); 19 | user.password = await this.cryptoService.generateHash(user.password); 20 | 21 | return await this.repository.save(user); 22 | } 23 | 24 | async findUserByEmail(email: string): Promise { 25 | return await this.repository.findOne({ email }); 26 | } 27 | 28 | async updateUser(id: string, data: UpdateUserDto): Promise { 29 | const user = await this.repository.findOne(id); 30 | 31 | if (!user) { 32 | throw new NotFoundException('User not found'); 33 | } 34 | 35 | await this.repository.update(id, { ...data }); 36 | 37 | return this.repository.create({ ...user, ...data }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { CryptoService } from '../../shared/services/crypto.service'; 4 | import { UsersController } from './controller/users.controller'; 5 | import { Users } from './entity/users.entity'; 6 | import { UsersService } from './service/users.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Users])], 10 | providers: [UsersService, CryptoService], 11 | controllers: [UsersController], 12 | exports: [UsersService], 13 | }) 14 | export class UsersModule {} 15 | -------------------------------------------------------------------------------- /src/shared/exception-filters/duplicate-key.exception-filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | Catch, 4 | ExceptionFilter, 5 | HttpException, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | import { Response } from 'express'; 9 | import { QueryFailedError } from 'typeorm'; 10 | 11 | @Catch(QueryFailedError) 12 | export class DuplicateKeyExceptionFilter implements ExceptionFilter { 13 | catch(exception: HttpException, host: ArgumentsHost) { 14 | const ctx = host.switchToHttp(); 15 | const response = ctx.getResponse(); 16 | 17 | response.status(HttpStatus.BAD_REQUEST).json({ 18 | statusCode: HttpStatus.BAD_REQUEST, 19 | message: 'There is already a client with that email', 20 | error: 'Bad Request', 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/services/crypto.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { compare, hash } from 'bcryptjs'; 3 | 4 | @Injectable() 5 | export class CryptoService { 6 | async generateHash(plainText: string): Promise { 7 | return await hash(plainText, 8); 8 | } 9 | 10 | async compareHash(plainText: string, hash: string): Promise { 11 | return await compare(plainText, hash); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/swagger/responses/auth.response.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class AuthResponse { 4 | @ApiProperty() 5 | token: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/swagger/responses/error.response.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ErrorResponse { 4 | @ApiProperty() 5 | statusCode: number; 6 | 7 | @ApiProperty() 8 | message: string; 9 | 10 | @ApiProperty() 11 | error: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/swagger/responses/users.response.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class UsersResponse { 4 | @ApiProperty() 5 | id: number; 6 | 7 | @ApiProperty() 8 | name: string; 9 | 10 | @ApiProperty() 11 | email: string; 12 | 13 | @ApiProperty() 14 | createdAt: Date; 15 | } 16 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/utils/test.uttil.ts: -------------------------------------------------------------------------------- 1 | import { Users } from '../../src/modules/users/entity/users.entity'; 2 | 3 | export class TestUtil { 4 | static getMockUser(): Users { 5 | const user = new Users(); 6 | user.id = 1; 7 | user.name = 'Any Name'; 8 | user.email = 'any@email.com'; 9 | user.password = 'any_password'; 10 | user.createdAt = new Date(); 11 | 12 | return user; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------