├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | Tecnologias |
25 | Projeto |
26 | Como rodar |
27 | Licença
28 |
29 |
30 |
31 |
32 |
33 |
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 |
--------------------------------------------------------------------------------