├── .dockerignore
├── .editorconfig
├── .env.example
├── .eslintrc
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .lintstagedrc
├── .prettierrc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── nest-cli.json
├── ormconfig.js
├── package-lock.json
├── package.json
├── src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── auth
│ ├── auth.module.ts
│ ├── auth.resolver.spec.ts
│ ├── auth.resolver.ts
│ ├── auth.service.spec.ts
│ ├── auth.service.ts
│ ├── dto
│ │ ├── auth.input.ts
│ │ └── auth.type.ts
│ ├── jwt-auth.guard.ts
│ └── jwt.strategy.ts
├── common
│ ├── test
│ │ └── TestUtil.ts
│ └── transformers
│ │ └── crypto-transform.ts
├── main.ts
└── users
│ ├── dto
│ ├── create-user.input.ts
│ └── update-user.input.ts
│ ├── user.entity.ts
│ ├── user.module.ts
│ ├── user.resolver.ts
│ ├── user.service.spec.ts
│ └── user.service.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | insert_final_newline = false
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | API_PORT=3003
2 |
3 | DB_HOST=postgres
4 | DB_USERNAME=postgres
5 | DB_PASSWORD=dbpass
6 | DB_NAME=angeloyt
7 | DB_SYNC=true
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "project": "tsconfig.json"
5 | },
6 | "plugins": [
7 | "@typescript-eslint/eslint-plugin"
8 | ],
9 | "extends": [
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended",
12 | "plugin:prettier/recommended",
13 | "prettier/@typescript-eslint",
14 | "prettier"
15 | ],
16 | "root": true,
17 | "env": {
18 | "node": true,
19 | "jest": true
20 | },
21 | "rules": {
22 | "@typescript-eslint/explicit-module-boundary-types": [
23 | "error"
24 | ],
25 | "@typescript-eslint/explicit-function-return-type": "off",
26 | "@typescript-eslint/interface-name-prefix": "off",
27 | "@typescript-eslint/no-explicit-any": "off",
28 | "@typescript-eslint/ban-ts-comment": "off",
29 | "prettier/prettier": [
30 | "error",
31 | {
32 | "singleQuote": true,
33 | "trailingComma": "all"
34 | }
35 | ]
36 | },
37 | "ignorePatterns": [
38 | "node_modules"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
4 |
5 | Fixes # (issue)
6 |
7 | ## Type of change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] This change requires a documentation update
15 |
16 | # How Has This Been Tested?
17 |
18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
19 |
20 | - [ ] Test A
21 | - [ ] Test B
22 |
23 | **Test Configuration**:
24 | * Operation System:
25 | * Browser:
26 | * Version:
27 |
28 | # Checklist:
29 |
30 | - [ ] My message follow de pattern text with emojis (See [Contributing guide](https://github.com/angelogluz/nest-angelo-youtube/CONTRIBUTING.md))
31 | - [ ] My code follows the style guidelines of this project
32 | - [ ] I have performed a self-review of my own code
33 | - [ ] I have commented my code, particularly in hard-to-understand areas
34 | - [ ] I have made corresponding changes to the documentation
35 | - [ ] My changes generate no new warnings
36 | - [ ] Any dependent changes have been merged and published in downstream modules
37 |
38 | # Additional information:
39 | Add any other relevant information about the pull request.
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # environment config
2 | .env
3 |
4 | # compiled output
5 | /dist
6 | /node_modules
7 |
8 | # Logs
9 | logs
10 | *.log
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | lerna-debug.log*
15 |
16 | # OS
17 | .DS_Store
18 |
19 | # Tests
20 | /coverage
21 | /.nyc_output
22 |
23 | # IDEs and editors
24 | /.idea
25 | .project
26 | .classpath
27 | .c9/
28 | *.launch
29 | .settings/
30 | *.sublime-workspace
31 |
32 | # IDE - VSCode
33 | .vscode/*
34 | !.vscode/settings.json
35 | !.vscode/tasks.json
36 | !.vscode/launch.json
37 | !.vscode/extensions.json
38 |
39 | # GraphQL
40 | src/schema.gql
41 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.ts": "eslint --cache --fix"
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Issues
2 |
3 | Se você encontrou um problema ou algo que possa ser melhorado, a abertura de uma Issue relatando o caso será bem-vinda em [bug report](https://github.com/angelogluz/Youtube-codes/issues).
4 | Antes disso, por favor, verifique se já não há uma relatando o mesmo problema.
5 |
6 | # Pull Requests
7 |
8 | Se você quer contribuir com o repositório, segue um guia rápido:
9 | 1. Tire um Fork do repositório
10 | 2. Desenvolva e teste seu código
11 |
12 | * Seu código, no mínimo, deve realizar build com sucesso no Travis (Quando habilitado)
13 | 3. Faça Commit das suas alterações:
14 |
15 | * Utilize mensagens claras para documentar o seu commit
16 | * Inclua um Emoji de no início da mensagem do pull request, que esteja de acordo com a tabela de emojis. Veja o emoji adequado [Emojis](#emojis)
17 | 4. Realize Push para o seu fork e em seguida envie um pull request to the **master** branch
18 |
19 | ## Emojis
20 |
21 | Quando estiver criando ou atualizando a mensagem do pull request
, por favor, **comece** a mensagem do commit com o emoji que identifique o pull request. Emojis não devem ser utilizados no título do pull request. Os emojis podem ser utilizados também nos commits
22 |
23 | * :new: `:new:` quando estiver adicionando uma nova funcionalidade
24 | * :bug: `:bug:` quando estiver corrigindo um bug
25 | * :memo: `:memo:` quando estiver adicionando documentação ao projeto
26 | * :art: `:art:` quando estiver aprimorando a estrutura do projeto
27 | * :fire: `:fire:` quando remover arquivos ou códigos
28 | * :racehorse: `:racehorse:` quando aprimorar performance
29 | * :white_check_mark: `:white_check_mark:` quando adicionar testes
30 | * :green_heart: `:green_heart:` quando corrigir erro de build do CI
31 | * :lock: `:lock:` quando desenvolver uma feature de segurança
32 |
33 | ## Leituras de apoio
34 | + [General GitHub documentation](https://help.github.com/)
35 | + [GitHub pull request documentation](https://help.github.com/send-pull-requests/)
36 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12 As development
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package.json .
6 | COPY package-lock.json .
7 |
8 | RUN npm install
9 |
10 | COPY . .
11 |
12 | RUN npm run build
13 |
14 | FROM node:12 as production
15 |
16 | ARG NODE_ENV=production
17 | ENV NODE_ENV=${NODE_ENV}
18 |
19 | WORKDIR /usr/src/app
20 |
21 | COPY package.json .
22 | COPY package-lock.json .
23 |
24 | RUN npm install --only=production
25 |
26 | COPY . .
27 |
28 | COPY --from=development /usr/src/app/dist ./dist
29 |
30 | CMD ["node", "dist/main"]
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Angelo Gonçalves da Luz
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 |
3 |
4 |
5 | Projeto de estudos gerado a partir dos vídeos do Canal do Youtube.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ## 🔐 Pré requisitos
25 |
26 | Docker
27 |
28 | Docker-compose
29 |
30 | ## 📹 Aulas
31 | Aula 01 - Entendendo GraphQL
32 |
33 | Aula 02 - Criando aplicação Nest/GraphQL
34 |
35 | Aula 03 - Dockerizando a aplicação
36 |
37 | Aula 04 - Testes unitários, mocks e cobertura de testes com Jest
38 |
39 | ## Para que o Typescript funcione no Host apropriadamente instale as dependências
40 |
41 | ```bash
42 | $ npm install
43 | ```
44 |
45 | ## Executando a aplicação
46 |
47 | ```bash
48 | # development
49 | $ docker-compose up
50 | ```
51 |
52 | ## Test
53 |
54 | ```bash
55 | # unit tests
56 | $ npm run test
57 |
58 | # e2e tests
59 | $ npm run test:e2e
60 |
61 | # test coverage
62 | $ npm run test:cov
63 | ```
64 |
65 |
66 | ## 🤔 Como contribuir
67 | Veja os detalhes de como contribuir [aqui](https://github.com/angelogluz/nest-angelo-youtube/blob/master/CONTRIBUTING.md).
68 |
69 | ## 🤝 Contribuídores
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ## License
78 |
79 | MIT License
80 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | api:
5 | container_name: nest_api
6 | build:
7 | context: .
8 | target: development
9 | volumes:
10 | - .:/usr/src/app
11 | - /usr/src/app/node_modules
12 | ports:
13 | - ${API_PORT}:${API_PORT}
14 | - 9229:9229
15 | command: npm run start:debug
16 | env_file:
17 | - .env
18 | networks:
19 | - backend
20 | depends_on:
21 | - postgres
22 | postgres:
23 | container_name: nest_db
24 | image: postgres:9.6-alpine
25 | networks:
26 | - backend
27 | environment:
28 | POSTGRES_PASSWORD: ${DB_PASSWORD}
29 | POSTGRES_USER: ${DB_USERNAME}
30 | POSTGRES_DB: ${DB_NAME}
31 | PG_DATA: /var/lib/postgresql/data
32 | ports:
33 | - 5432
34 | volumes:
35 | - api_db_data:/var/lib/postgresql/data
36 | networks:
37 | backend:
38 | volumes:
39 | api_db_data:
40 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src",
4 | "compilerOptions": {
5 | "plugins": [
6 | {
7 | "name": "@nestjs/graphql/plugin",
8 | "options": {
9 | "typeFileNameSuffix": [".input.ts", ".args.ts", ".entity.ts", ".type.ts"]
10 | }
11 | }
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ormconfig.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | type: 'postgres',
3 | host: process.env.DB_HOST,
4 | port: +process.env.DB_PORT || 5432,
5 | username: process.env.DB_USERNAME,
6 | password: process.env.DB_PASSWORD,
7 | database: process.env.DB_NAME,
8 | entities: ['dist/**/*.entity{.ts,.js}'],
9 | synchronize: process.env.DB_SYNC == 'true',
10 | };
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angeloyt",
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:docker:dev": "npm install && nest start --watch",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:ver": "jest --verbose",
20 | "test:watch": "jest --watch",
21 | "test:cov": "jest --coverage",
22 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
23 | "test:e2e": "jest --config ./test/jest-e2e.json"
24 | },
25 | "dependencies": {
26 | "@nestjs/common": "^7.0.0",
27 | "@nestjs/core": "^7.0.0",
28 | "@nestjs/graphql": "^7.6.0",
29 | "@nestjs/jwt": "^7.1.0",
30 | "@nestjs/passport": "^7.1.0",
31 | "@nestjs/platform-express": "^7.0.0",
32 | "@nestjs/typeorm": "^7.1.0",
33 | "apollo-server-express": "^2.16.1",
34 | "bcrypt": "^5.0.0",
35 | "class-transformer": "^0.3.1",
36 | "class-validator": "^0.12.2",
37 | "graphql": "^15.3.0",
38 | "graphql-tools": "^6.0.15",
39 | "passport": "^0.4.1",
40 | "passport-jwt": "^4.0.0",
41 | "pg": "^8.3.0",
42 | "reflect-metadata": "^0.1.13",
43 | "rimraf": "^3.0.2",
44 | "rxjs": "^6.5.4",
45 | "typeorm": "^0.2.25"
46 | },
47 | "devDependencies": {
48 | "@nestjs/cli": "^7.0.0",
49 | "@nestjs/schematics": "^7.0.0",
50 | "@nestjs/testing": "^7.0.0",
51 | "@types/bcrypt": "^3.0.0",
52 | "@types/express": "^4.17.3",
53 | "@types/jest": "25.2.3",
54 | "@types/node": "^13.9.1",
55 | "@types/passport-jwt": "^3.0.3",
56 | "@types/supertest": "^2.0.8",
57 | "@typescript-eslint/eslint-plugin": "3.0.2",
58 | "@typescript-eslint/parser": "3.0.2",
59 | "eslint": "7.1.0",
60 | "eslint-config-prettier": "^6.10.0",
61 | "eslint-plugin-import": "^2.22.1",
62 | "eslint-plugin-jest": "^24.1.0",
63 | "eslint-plugin-prettier": "^3.1.4",
64 | "jest": "26.0.1",
65 | "lint-staged": "^10.4.0",
66 | "prettier": "^1.19.1",
67 | "supertest": "^4.0.2",
68 | "ts-jest": "26.1.0",
69 | "ts-loader": "^6.2.1",
70 | "ts-node": "^8.6.2",
71 | "tsconfig-paths": "^3.9.0",
72 | "typescript": "^3.7.4"
73 | },
74 | "jest": {
75 | "moduleFileExtensions": [
76 | "js",
77 | "json",
78 | "ts"
79 | ],
80 | "rootDir": "src",
81 | "testRegex": ".spec.ts$",
82 | "transform": {
83 | "^.+\\.(t|j)s$": "ts-jest"
84 | },
85 | "coverageDirectory": "../coverage",
86 | "testEnvironment": "node"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 |
3 | import { AppController } from './app.controller';
4 | import { AppService } from './app.service';
5 |
6 | describe('AppController', () => {
7 | let appController: AppController;
8 |
9 | beforeEach(async () => {
10 | const app: TestingModule = await Test.createTestingModule({
11 | controllers: [AppController],
12 | providers: [AppService],
13 | }).compile();
14 |
15 | appController = app.get(AppController);
16 | });
17 |
18 | describe('root', () => {
19 | it('should return "Hello World!"', () => {
20 | expect(appController.getHello()).toBe('Hello World!');
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 |
3 | import { AppService } from './app.service';
4 |
5 | @Controller()
6 | export class AppController {
7 | constructor(private readonly appService: AppService) {}
8 |
9 | @Get()
10 | getHello(): string {
11 | return this.appService.getHello();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 | import { GraphQLModule } from '@nestjs/graphql';
4 | import { join } from 'path';
5 |
6 | import { AppController } from './app.controller';
7 | import { AppService } from './app.service';
8 | import { UserModule } from './users/user.module';
9 | import { AuthModule } from './auth/auth.module';
10 |
11 | @Module({
12 | imports: [
13 | TypeOrmModule.forRoot(),
14 | GraphQLModule.forRoot({
15 | autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
16 | context: ({ req }) => ({ req }),
17 | }),
18 | UserModule,
19 | AuthModule,
20 | ],
21 | controllers: [AppController],
22 | providers: [AppService],
23 | })
24 | export class AppModule {}
25 |
--------------------------------------------------------------------------------
/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AuthService } from './auth.service';
3 | import { AuthResolver } from './auth.resolver';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | import { User } from 'src/users/user.entity';
6 | import { UserService } from 'src/users/user.service';
7 | import { JwtModule } from '@nestjs/jwt';
8 | import { JwtStrategy } from './jwt.strategy';
9 |
10 | @Module({
11 | imports: [
12 | TypeOrmModule.forFeature([User]),
13 | JwtModule.registerAsync({
14 | useFactory: () => ({
15 | secret: process.env.JWT_SECRET,
16 | signOptions: {
17 | expiresIn: '30s'
18 | },
19 | }),
20 | }),
21 | ],
22 | providers: [AuthService, AuthResolver, UserService, JwtStrategy]
23 | })
24 | export class AuthModule {}
25 |
--------------------------------------------------------------------------------
/src/auth/auth.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthResolver } from './auth.resolver';
3 |
4 | describe('AuthResolver', () => {
5 | let resolver: AuthResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [AuthResolver],
10 | }).compile();
11 |
12 | resolver = module.get(AuthResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/auth/auth.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Args, Mutation, Resolver } from '@nestjs/graphql';
2 | import { AuthService } from './auth.service';
3 | import { AuthInput } from './dto/auth.input';
4 | import { AuthType } from './dto/auth.type';
5 |
6 | @Resolver('Auth')
7 | export class AuthResolver {
8 | constructor(private authService: AuthService) { }
9 |
10 | @Mutation(() => AuthType)
11 | public async login(
12 | @Args('data') data: AuthInput
13 | ): Promise {
14 | const response = await this.authService.validateUser(data);
15 |
16 | return {
17 | user: response.user,
18 | token: response.token
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/auth/auth.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthService } from './auth.service';
3 |
4 | describe('AuthService', () => {
5 | let service: AuthService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [AuthService],
10 | }).compile();
11 |
12 | service = module.get(AuthService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, UnauthorizedException } from '@nestjs/common';
2 | import { JwtService } from '@nestjs/jwt';
3 | import { compareSync } from 'bcrypt';
4 | import { subscribe } from 'graphql';
5 | import { User } from 'src/users/user.entity';
6 | import { UserService } from 'src/users/user.service';
7 | import { AuthInput } from './dto/auth.input';
8 | import { AuthType } from './dto/auth.type';
9 |
10 | @Injectable()
11 | export class AuthService {
12 | constructor(
13 | private userService: UserService,
14 | private jwtService: JwtService
15 | ) {}
16 |
17 | async validateUser(data: AuthInput): Promise {
18 | const user = await this.userService.getUserByEmail(data.email);
19 |
20 | const validPasssword = compareSync(data.password, user.password);
21 |
22 | if(!validPasssword) {
23 | throw new UnauthorizedException('Incorrect password')
24 | }
25 |
26 | const token = await this.jwtToken(user);
27 |
28 | return {
29 | user,
30 | token
31 | }
32 | }
33 |
34 | private async jwtToken(user: User): Promise {
35 | const payload = { username: user.name, sub: user.id };
36 | return this.jwtService.signAsync(payload);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/auth/dto/auth.input.ts:
--------------------------------------------------------------------------------
1 | import { InputType } from "@nestjs/graphql";
2 |
3 | @InputType()
4 | export class AuthInput {
5 | email: string;
6 | password: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/auth/dto/auth.type.ts:
--------------------------------------------------------------------------------
1 | import { Field, ObjectType } from "@nestjs/graphql";
2 | import { User } from "src/users/user.entity";
3 |
4 | @ObjectType()
5 | export class AuthType {
6 | @Field(() => User)
7 | user: User;
8 |
9 | token: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/auth/jwt-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { ExecutionContext, Injectable } from '@nestjs/common';
2 | import { GqlExecutionContext } from '@nestjs/graphql';
3 | import { AuthGuard } from '@nestjs/passport';
4 | import { Request } from 'express';
5 |
6 | @Injectable()
7 | export class GqlAuthGuard extends AuthGuard('jwt') {
8 | getRequest(context: ExecutionContext): Request {
9 | const ctx = GqlExecutionContext.create(context);
10 | return ctx.getContext().req;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/auth/jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { ExtractJwt, Strategy } from 'passport-jwt';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Injectable, UnauthorizedException } from '@nestjs/common';
4 | import { User } from 'src/users/user.entity';
5 | import { UserService } from 'src/users/user.service';
6 |
7 | @Injectable()
8 | export class JwtStrategy extends PassportStrategy(Strategy) {
9 | constructor(
10 | private userService: UserService
11 | ) {
12 | super({
13 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
14 | ignoreExpiration: false,
15 | secretOrKey: process.env.JWT_SECRET
16 | });
17 | }
18 |
19 | async validate(payload: {sub: User['id'], name: string}) {
20 | const user = this.userService.getUserById(payload.sub);
21 |
22 | if(!user) {
23 | throw new UnauthorizedException('Unauthorized');
24 | }
25 |
26 | return user;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/common/test/TestUtil.ts:
--------------------------------------------------------------------------------
1 | // import { CreateUserInput } from 'src/users/dto/create-user.input';
2 | // import { UpdateUserInput } from 'src/users/dto/update-user.input';
3 | // import { User } from './../../users/user.entity';
4 |
5 | // export const mockAddAccountParams: CreateUserInput = {
6 | // name: 'Test User',
7 | // email: 'user@email.com',
8 | // };
9 |
10 | // export const mockUpdateUserParams: UpdateUserInput = {
11 | // id: '1',
12 | // email: 'email-updated@email.com',
13 | // };
14 |
15 | // export const mockUserModel: User = {
16 | // id: '1',
17 | // ...mockAddAccountParams,
18 | // };
19 |
20 | // export const mockUpdatedUserModel: User = {
21 | // ...mockUserModel,
22 | // email: 'updated-email@email.com',
23 | // };
24 |
25 | // export const mockUserArrayModel: User[] = [
26 | // mockUserModel,
27 | // {
28 | // id: '2',
29 | // name: 'Test User 2',
30 | // email: 'email2@email.com',
31 | // },
32 | // {
33 | // id: '3',
34 | // name: 'Test User 3',
35 | // email: 'email3@email.com',
36 | // },
37 | // ];
38 |
--------------------------------------------------------------------------------
/src/common/transformers/crypto-transform.ts:
--------------------------------------------------------------------------------
1 | import { hashSync } from "bcrypt"
2 |
3 | export const hashPasswordTransform = {
4 | to(password: string): string {
5 | return hashSync(password, 10);
6 | },
7 | from(hash: string): string {
8 | return hash;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { ValidationPipe } from '@nestjs/common';
3 |
4 | import { AppModule } from './app.module';
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule);
8 | app.useGlobalPipes(new ValidationPipe());
9 | await app.listen(process.env.API_PORT || 3000);
10 | }
11 | bootstrap();
12 |
--------------------------------------------------------------------------------
/src/users/dto/create-user.input.ts:
--------------------------------------------------------------------------------
1 | import { InputType } from '@nestjs/graphql';
2 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
3 |
4 | @InputType()
5 | export class CreateUserInput {
6 | @IsString()
7 | @IsNotEmpty({ message: 'Invalid characters' })
8 | name: string;
9 |
10 | @IsEmail()
11 | @IsNotEmpty({ message: 'Invalid E-mail' })
12 | email: string;
13 |
14 | @IsString()
15 | @IsNotEmpty({ message: 'Password is required' })
16 | password: string;
17 | }
18 |
--------------------------------------------------------------------------------
/src/users/dto/update-user.input.ts:
--------------------------------------------------------------------------------
1 | import { InputType } from '@nestjs/graphql';
2 | import {
3 | IsEmail,
4 | IsNotEmpty,
5 | IsOptional,
6 | IsString,
7 | IsUUID,
8 | } from 'class-validator';
9 |
10 | @InputType()
11 | export class UpdateUserInput {
12 | @IsString()
13 | @IsOptional()
14 | @IsUUID()
15 | id?: string;
16 |
17 | @IsOptional()
18 | @IsString()
19 | @IsNotEmpty({ message: 'Invalid characters' })
20 | name?: string;
21 |
22 | @IsOptional()
23 | @IsEmail()
24 | @IsNotEmpty({ message: 'Invalid E-mail' })
25 | email?: string;
26 |
27 | @IsOptional()
28 | @IsString()
29 | @IsNotEmpty({ message: 'Password is required' })
30 | password?: string;
31 | }
32 |
--------------------------------------------------------------------------------
/src/users/user.entity.ts:
--------------------------------------------------------------------------------
1 | import { ObjectType, Field, ID, HideField } from '@nestjs/graphql';
2 | import { hashPasswordTransform } from 'src/common/transformers/crypto-transform';
3 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
4 |
5 | @ObjectType()
6 | @Entity()
7 | export class User {
8 | @PrimaryGeneratedColumn('uuid')
9 | @Field(() => ID)
10 | id: string;
11 |
12 | @Column()
13 | name: string;
14 |
15 | @Column()
16 | email: string;
17 |
18 | @Column({
19 | transformer: hashPasswordTransform
20 | })
21 | @HideField()
22 | password: string;
23 | }
24 |
--------------------------------------------------------------------------------
/src/users/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 |
4 | import { UserService } from './user.service';
5 | import { UserResolver } from './user.resolver';
6 | import { User } from './user.entity';
7 |
8 | @Module({
9 | imports: [TypeOrmModule.forFeature([User])],
10 | providers: [UserService, UserResolver],
11 | })
12 | export class UserModule {}
13 |
--------------------------------------------------------------------------------
/src/users/user.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Resolver, Query, Args, Mutation } from '@nestjs/graphql';
2 |
3 | import { User } from './user.entity';
4 | import { UserService } from './user.service';
5 | import { CreateUserInput } from './dto/create-user.input';
6 | import { UpdateUserInput } from './dto/update-user.input';
7 | import { UseGuards } from '@nestjs/common';
8 | import { GqlAuthGuard } from '../auth/jwt-auth.guard';
9 |
10 | @Resolver('User')
11 | export class UserResolver {
12 | constructor(private readonly userService: UserService) {}
13 |
14 | @Mutation(() => User)
15 | async createUser(@Args('data') data: CreateUserInput): Promise {
16 | return this.userService.createUser(data);
17 | }
18 |
19 | @UseGuards(GqlAuthGuard)
20 | @Query(() => User)
21 | async user(@Args('id') id: string): Promise {
22 | return this.userService.getUserById(id);
23 | }
24 |
25 | @Query(() => User)
26 | async userByEmail(@Args('email') email: string): Promise {
27 | return this.userService.getUserByEmail(email);
28 | }
29 |
30 | @Query(() => [User])
31 | async users(): Promise {
32 | return await this.userService.findAllUsers();
33 | }
34 |
35 | @Mutation(() => User)
36 | async updateUser(
37 | @Args('id') id: string,
38 | @Args('data') data: UpdateUserInput,
39 | ): Promise {
40 | return this.userService.updateUser({ id, ...data });
41 | }
42 |
43 | @Mutation(() => Boolean)
44 | async deleteUser(@Args('id') id: string): Promise {
45 | await this.userService.deleteUser(id);
46 | return true;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/users/user.service.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NotFoundException,
3 | InternalServerErrorException,
4 | } from '@nestjs/common';
5 | import { getRepositoryToken } from '@nestjs/typeorm';
6 | import { Test, TestingModule } from '@nestjs/testing';
7 |
8 | import { User } from './user.entity';
9 | import { UserService } from './user.service';
10 | import {
11 | mockAddAccountParams,
12 | mockUpdateUserParams,
13 | mockUpdatedUserModel,
14 | mockUserModel,
15 | mockUserArrayModel,
16 | } from './../common/test/TestUtil';
17 |
18 | describe('UserService', () => {
19 | let service: UserService;
20 |
21 | const mockRepository = {
22 | find: jest.fn().mockReturnValue(mockUserArrayModel),
23 | findOne: jest.fn().mockReturnValue(mockUserModel),
24 | create: jest.fn().mockReturnValue(mockUserModel),
25 | save: jest.fn().mockReturnValue(mockUserModel),
26 | update: jest.fn().mockReturnValue(mockUpdatedUserModel),
27 | delete: jest.fn().mockReturnValue({ affected: 1 }),
28 | };
29 |
30 | beforeEach(async () => {
31 | const module: TestingModule = await Test.createTestingModule({
32 | providers: [
33 | UserService,
34 | {
35 | provide: getRepositoryToken(User),
36 | useValue: mockRepository,
37 | },
38 | ],
39 | }).compile();
40 |
41 | service = module.get(UserService);
42 | });
43 |
44 | it('Should be defined', () => {
45 | expect(service).toBeDefined();
46 | });
47 |
48 | describe('When search all Users', () => {
49 | it('Should list all users', async () => {
50 | const users = service.findAllUsers();
51 |
52 | expect(users).resolves.toBe(mockUserArrayModel);
53 | expect(mockRepository.find).toHaveBeenCalledTimes(1);
54 | });
55 | });
56 |
57 | describe('When search User By Id', () => {
58 | it('Should find a existing user', async () => {
59 | const userFound = service.getUserById('1');
60 |
61 | expect(mockRepository.findOne).toHaveBeenCalledWith(mockUserModel.id);
62 | expect(userFound).resolves.toBe(mockUserModel);
63 | });
64 | it('Should return a exception when does not to find a user', async () => {
65 | mockRepository.findOne.mockReturnValue(null);
66 |
67 | const user = service.getUserById('3');
68 |
69 | expect(user).rejects.toThrow(NotFoundException);
70 | expect(mockRepository.findOne).toHaveBeenCalledWith('3');
71 | });
72 | });
73 |
74 | describe('When create a user', () => {
75 | it('Should create a user', async () => {
76 | const user = service.createUser(mockAddAccountParams);
77 |
78 | expect(mockRepository.create).toBeCalledWith(mockAddAccountParams);
79 | expect(mockRepository.save).toBeCalledTimes(1);
80 | expect(user).resolves.toBe(mockUserModel);
81 | });
82 | });
83 |
84 | describe('When update User', () => {
85 | it('Should update a user', async () => {
86 | service.getUserById = jest.fn().mockReturnValueOnce(mockUserModel);
87 |
88 | const userUpdated = service.updateUser(mockUpdateUserParams);
89 |
90 | expect(service.getUserById).toHaveBeenCalledWith(mockUpdateUserParams.id);
91 | expect(userUpdated).resolves.toBe(mockUpdatedUserModel);
92 | });
93 |
94 | describe('When delete User', () => {
95 | it('Should delete a existing user', async () => {
96 | service.getUserById = jest.fn().mockReturnValueOnce(mockUserModel);
97 |
98 | await service.deleteUser('1');
99 |
100 | expect(service.getUserById).toHaveBeenCalledWith('1');
101 | expect(mockRepository.delete).toBeCalledWith(mockUserModel);
102 | });
103 |
104 | it('Should return an internal server error if repository does not delete the user', async () => {
105 | service.getUserById = jest.fn().mockReturnValueOnce(mockUserModel);
106 | mockRepository.delete.mockReturnValueOnce(null);
107 |
108 | const deletedUser = service.deleteUser('1');
109 |
110 | expect(service.getUserById).toHaveBeenCalledWith('1');
111 | expect(mockRepository.delete).toBeCalledWith(mockUserModel);
112 | expect(deletedUser).rejects.toThrow(InternalServerErrorException);
113 | });
114 | });
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/src/users/user.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Injectable,
3 | NotFoundException,
4 | InternalServerErrorException,
5 | } from '@nestjs/common';
6 | import { InjectRepository } from '@nestjs/typeorm';
7 | import { Repository } from 'typeorm';
8 |
9 | import { CreateUserInput } from './dto/create-user.input';
10 | import { UpdateUserInput } from './dto/update-user.input';
11 | import { User } from './user.entity';
12 |
13 | @Injectable()
14 | export class UserService {
15 | constructor(
16 | @InjectRepository(User)
17 | private userRepository: Repository,
18 | ) {}
19 |
20 | async createUser(data: CreateUserInput): Promise {
21 | const user = this.userRepository.create(data);
22 | return this.userRepository.save(user);
23 | }
24 |
25 | async getUserById(id: string): Promise {
26 | const user = await this.userRepository.findOne(id);
27 | if (!user) {
28 | throw new NotFoundException('User not found');
29 | }
30 | return user;
31 | }
32 |
33 | async getUserByEmail(email: string): Promise {
34 | const user = await this.userRepository.findOne({where: { email }});
35 | if (!user) {
36 | throw new NotFoundException('User not found');
37 | }
38 | return user;
39 | }
40 |
41 | async findAllUsers(): Promise {
42 | return await this.userRepository.find();
43 | }
44 |
45 | async updateUser(data: UpdateUserInput): Promise {
46 | const user = await this.getUserById(data.id);
47 | return this.userRepository.save({ ...user, ...data });
48 | }
49 |
50 | async deleteUser(id: string): Promise {
51 | const user = await this.getUserById(id);
52 | const userDeleted = await this.userRepository.delete(user);
53 | if (!userDeleted) {
54 | throw new InternalServerErrorException();
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------