├── .DS_Store ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .vscode └── launch.json ├── README.md ├── jest.config.js ├── package.json ├── prettier.config.js ├── src ├── @types │ └── express.d.ts ├── config │ ├── auth.ts │ ├── cache.ts │ ├── mail.ts │ └── upload.ts ├── modules │ ├── appointments │ │ ├── dtos │ │ │ ├── ICreateAppointmentDTO.ts │ │ │ ├── IFindAllInDayFromProviderDTO.ts │ │ │ └── IFindAllInMonthFromProviderDTO.ts │ │ ├── infra │ │ │ ├── http │ │ │ │ ├── controllers │ │ │ │ │ ├── AppointmentsController.ts │ │ │ │ │ ├── ProviderAppointmentsController.ts │ │ │ │ │ ├── ProviderDayAvailabilityController.ts │ │ │ │ │ ├── ProviderMonthAvailabilityController.ts │ │ │ │ │ └── ProvidersController.ts │ │ │ │ └── routes │ │ │ │ │ ├── appointments.routes.ts │ │ │ │ │ └── providers.routes.ts │ │ │ └── typeorm │ │ │ │ ├── entities │ │ │ │ └── Appointment.ts │ │ │ │ └── repositories │ │ │ │ └── AppointmentsRepository.ts │ │ ├── repositories │ │ │ ├── IAppointmentsRepository.ts │ │ │ └── fakes │ │ │ │ └── FakeAppointmentsRepository.ts │ │ └── services │ │ │ ├── CreateAppointmentService.spec.ts │ │ │ ├── CreateAppointmentService.ts │ │ │ ├── ListProviderAppointmentsService.spec.ts │ │ │ ├── ListProviderAppointmentsService.ts │ │ │ ├── ListProviderDayAvailabilityService.spec.ts │ │ │ ├── ListProviderDayAvailabilityService.ts │ │ │ ├── ListProviderMonthAvailabilityService.spec.ts │ │ │ ├── ListProviderMonthAvailabilityService.ts │ │ │ ├── ListProvidersService.spec.ts │ │ │ └── ListProvidersService.ts │ ├── notifications │ │ ├── dtos │ │ │ └── ICreateNotificationDTO.ts │ │ ├── infra │ │ │ └── typeorm │ │ │ │ ├── repositories │ │ │ │ └── NotificationsRepository.ts │ │ │ │ └── schemas │ │ │ │ └── Notification.ts │ │ └── repositories │ │ │ ├── INotificationsRepository.ts │ │ │ └── fakes │ │ │ └── FakeNotificationsRepository.ts │ └── users │ │ ├── dtos │ │ ├── ICreateUserDTO.ts │ │ └── IFindAllProvidersDTO.ts │ │ ├── infra │ │ ├── http │ │ │ ├── controllers │ │ │ │ ├── ForgotPasswordController.ts │ │ │ │ ├── ProfileController.ts │ │ │ │ ├── ResetPasswordController.ts │ │ │ │ ├── SessionsController.ts │ │ │ │ ├── UserAvatarController.ts │ │ │ │ └── UsersController.ts │ │ │ ├── middlewares │ │ │ │ └── ensureAuthenticated.ts │ │ │ └── routes │ │ │ │ ├── password.routes.ts │ │ │ │ ├── profile.routes.ts │ │ │ │ ├── sessions.routes.ts │ │ │ │ └── users.routes.ts │ │ └── typeorm │ │ │ ├── entities │ │ │ ├── User.ts │ │ │ └── UserToken.ts │ │ │ └── repositories │ │ │ ├── UserTokensRepository.ts │ │ │ └── UsersRepository.ts │ │ ├── providers │ │ ├── HashProvider │ │ │ ├── fakes │ │ │ │ └── FakeHashProvider.ts │ │ │ ├── implementations │ │ │ │ └── BCryptHashProvider.ts │ │ │ └── models │ │ │ │ └── IHashProvider.ts │ │ └── index.ts │ │ ├── repositories │ │ ├── IUserTokensRepository.ts │ │ ├── IUsersRepository.ts │ │ └── fakes │ │ │ ├── FakeUserTokensRepository.ts │ │ │ └── FakeUsersRepository.ts │ │ ├── services │ │ ├── AuthenticateUserService.spec.ts │ │ ├── AuthenticateUserService.ts │ │ ├── CreateUserService.spec.ts │ │ ├── CreateUserService.ts │ │ ├── ResetPasswordService.spec.ts │ │ ├── ResetPasswordService.ts │ │ ├── SendForgotPasswordEmailService.spec.ts │ │ ├── SendForgotPasswordEmailService.ts │ │ ├── ShowProfileService.spec.ts │ │ ├── ShowProfileService.ts │ │ ├── UpdateProfileService.spec.ts │ │ ├── UpdateProfileService.ts │ │ ├── UpdateUserAvatarService.spec.ts │ │ └── UpdateUserAvatarService.ts │ │ └── views │ │ └── forgot_password.hbs └── shared │ ├── container │ ├── index.ts │ └── providers │ │ ├── CacheProvider │ │ ├── implementations │ │ │ ├── RedisCacheProvider.ts │ │ │ └── fakes │ │ │ │ └── FakeCacheProvider.ts │ │ ├── index.ts │ │ └── models │ │ │ └── ICacheProvider.ts │ │ ├── MailProvider │ │ ├── dtos │ │ │ └── ISendMailDTO.ts │ │ ├── fakes │ │ │ └── FakeMailProvider.ts │ │ ├── implementations │ │ │ ├── CustomMailProvider.ts │ │ │ └── EtherealMailProvider.ts │ │ ├── index.ts │ │ └── models │ │ │ └── IMailProvider.ts │ │ ├── MailTemplateProvider │ │ ├── dtos │ │ │ └── IParseMailTemplateDTO.ts │ │ ├── fakes │ │ │ └── FakeMailTemplateProvider.ts │ │ ├── implementations │ │ │ └── HbsMailTemplateProvider.ts │ │ ├── index.ts │ │ └── models │ │ │ └── IMailTemplateProvider.ts │ │ ├── StorageProvider │ │ ├── fakes │ │ │ └── FakeStorageProvider.ts │ │ ├── implementations │ │ │ ├── DiskStorageProvider.ts │ │ │ └── S3StorageProvider.ts │ │ ├── index.ts │ │ └── models │ │ │ └── IStorageProvider.ts │ │ └── index.ts │ ├── errors │ └── AppError.ts │ └── infra │ ├── http │ ├── middlewares │ │ └── rateLimiter.ts │ ├── routes │ │ └── index.ts │ └── server.ts │ └── typeorm │ ├── index.ts │ └── migrations │ ├── 1586877055453-CreateUsers.ts │ ├── 1586888144285-CreateAppointments.ts │ ├── 1586981197413-AddAvatarFieldToUsers.ts │ ├── 1588899359097-CreateUserTokens.ts │ └── 1589120070157-AddUserIdToAppointment.ts ├── temp └── .gitkeep ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgmarinho/gobarber-api-gostack11/6b4b30121a044ce442f8dca7e404ef687190218c/.DS_Store -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true # (true) tira os espaços que sobram no final da linha 8 | insert_final_newline = true # (true) coloca uma linha em branco no final do arquivo 9 | end_of_line = lf # linux e windows entender p \n 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # App 2 | APP_SECRET= 3 | APP_WEB_URL=http://localhost:3000 4 | APP_API_URL=http://localhost:3333 5 | 6 | # Mail 7 | MAIL_DRIVER=ethereal 8 | 9 | #Storage 10 | STORAGE_DRIVER=disk 11 | 12 | #Redis 13 | REDIS_HOST=localhost 14 | REDIS_PORT=6379 15 | REDIS_PASS= 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /*.js 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "airbnb-base", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended" 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 2018, 20 | "sourceType": "module" 21 | }, 22 | "plugins": ["@typescript-eslint", "prettier"], 23 | "rules": { 24 | "class-methods-use-this": "off", 25 | "@typescript-eslint/camelcase": "off", 26 | "@typescript-eslint/no-unused-vars": [ 27 | "error", 28 | { "argsIgnorePattern": "_" } 29 | ], 30 | "@typescript-eslint/interface-name-prefix": [ 31 | "error", 32 | { "prefixWithI": "always" } 33 | ], 34 | "prettier/prettier": "error", 35 | "import/extensions": ["error", "ignorePackages", { "ts": "never" }], 36 | "no-useless-constructor": "off" 37 | }, 38 | "settings": { 39 | "import/resolver": { 40 | "typescript": {} 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | buid/ 3 | .idea/ 4 | .vscode/ 5 | 6 | temp/* 7 | !temp/.gitkeep 8 | coverage/ 9 | .DS_Store 10 | .env 11 | ormconfig.json 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "protocol": "inspector", 11 | "restart": true, 12 | "name": "Debug", 13 | "skipFiles": ["/**"], 14 | "outFiles": ["${workspaceFolder}/**/*.js"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |
5 |

6 | 7 |

8 |
9 | 10 |

GoBarber - Web Aplication

11 | 12 |
13 |

14 | licenceMIT 15 |

16 |
17 | 18 | ## :speech_balloon: Sobre 19 | 20 | API GoBarber, aplicação que conecta prestadores de serviço (Barbeiros e Cabeleireiros) aos clientes em suas regiões. Aplicação 21 | montada durante o bootcamp GoStack aplicando todo o conhecimento adquirido durante a jornada. Neste projeto foi utilizada as 22 | melhores práticas na construção do projeto, com o uso das tecnologias TypeScript, Express, TypeORM em cima do Ambiente e execução 23 | de javascript, o NodeJS. 24 | 25 | Faz parte do projeto GoBarber 26 | 27 | - [Web Aplication](https://github.com/tgmarinho/gobarber-web-aplication): Aplicação Web contruida em ReactJs 28 | - [Mobile Aplication](https://github.com/tgmarinho/gobarber-mobile-aplication): Aplicação Mobile construida em React Native. 29 | 30 | ## :rocket: Tecnologias 31 | 32 | - [TypeScript](https://www.typescriptlang.org/): Linguagem. 33 | - [NodeJs](https://nodejs.org/en/): Ambiente de Execução. 34 | - [Express](https://expressjs.com/): API Framework 35 | - [JsonWebToken](https://github.com/auth0/node-jsonwebtoken): Autenticação JWT 36 | - [Multer](https://github.com/expressjs/multer): Upload de Arquivos 37 | - [Postgres](https://www.postgresql.org/): Banco de Dados 38 | - [TypeORM](https://typeorm.io/#/): ORM 39 | - [Eslint](https://eslint.org/): Padronização de código 40 | - [Jest](https://jestjs.io/): Testes 41 | - [tsyringe](https://github.com/microsoft/tsyringe): Lib de injeção de dependencias. -[uuidv4](https://github.com/thenativeweb/uuidv4#readme): uuid. 42 | 43 | :warning: Durante o desenvolvimento irei atualizando a lista de tecnologia 44 | 45 | ## 🔖 Layout 46 | 47 | Uma API Rest, que retorna o conteúdo em JSON que vai ser consumida tanto por um Front-end em [ReactJS](https://reactjs.org/) quanto por uma aplicação Mobile Hibrido com [React Native](https://reactnative.dev/). 48 | 49 | ### Base da Aplicação. 50 | 51 | Requisitos funcionais: 52 | [] 100% de cobertura de testes nos services da aplicação. 53 | [] Tratamento de exceções global 54 | 55 | Requisitos Não Funcionais: 56 | - Framework da API - Express 57 | - Linguagem de Programação - TypeScript 58 | - Banco de dados utilizado na aplicação - Postgres 59 | - ORM - TypeORM 60 | - Lib de testes - Jest 61 | - Utilizar Mailtrap para testar envios de email em ambiente de desenvolvimento 62 | - Utilizar Amazon SES para envios de email em ambiente de Produção. 63 | - Utilizar Eslint, Prettier e EditorConfig para padronizar o código em ambiente de desenvolvimento, com a style guide do AirBnb 64 | 65 | ### Criação de usuário 66 | 67 | Requisitos Funcionais: 68 | [x] Criação de conta com (Nome, Email, Senha); 69 | [] Envio de email confirmando criação de conta; 70 | 71 | Requisitos Não Funcionais: 72 | - Envio de email utilizando lib Nodemailer; 73 | 74 | Regras de Negócio: 75 | [] Não pode ser criado duas contas com o mesmo email; 76 | [] O usuário deve confirmar a senha ao criar uma conta. 77 | [] A senha deve ser Hasheada antes de ser gravada no banco de dados; 78 | 79 | ### Autenticação 80 | 81 | Requisitos Funcionais: 82 | [] O usuário deve poder se Autenticar utilizando email e senha; 83 | 84 | Requisitos Não Funcionais: 85 | - A autenticação deve ser feita com Json Web Token (JWT); 86 | 87 | Regras de Negócio: 88 | [x] No payload do token deve ser armazenado o ID do usuário; 89 | 90 | ### Recuperação de Senha 91 | 92 | Requisitos Funcionais: 93 | [x] O usuário deve poder recuperar sua senha informando o seu email; 94 | [x] O usuário de receber um email com instruções de recuperação de senha; 95 | [x] O usuário deve poder resetar sua senha ; 96 | 97 | Requisitos Não Funcionais: 98 | - Envio de email utilizando lib Nodemailer; 99 | - O envio de email deve acontecer em segundo plano (background job); 100 | 101 | 102 | Regras de Negócio: 103 | [x] O link enviado por email para resetar a senha, deve expirar em 2h; 104 | [x] O usuário precisa confirmar a nova senha ao resetar sua senha. 105 | 106 | ### Atualização de Perfil 107 | 108 | Requisitos Funcionais: 109 | [] O usuário deve poder atualizar seu perfil (nome, email, senha, Avatar); 110 | 111 | Regras de Negócio: 112 | [x] O usuário não pode alterar seu email para um email ja em uso na aplicação 113 | [x] Para atulizar sua senha, o usuário deve informar a senha antiga; 114 | [x] Para atulizar sua senha, o usuário precisa confirmar a senha; 115 | 116 | ### Painel de usuário (Prestador de serviço) 117 | 118 | Requisitos Funcionais: 119 | [] O prestador deve poder listar os seus agendamentos de um dia especifico; 120 | [] O prestador deve poder receber uma notificação sempre que houver um novo agendamento; 121 | [] O prestador deve poder visualizar as notificações não lidas; 122 | 123 | 124 | Requisitos Não Funcionais: 125 | - Os agendamentos devem ser armazenados em cache. 126 | - As notificações do prestador devem ser armazenadas no MongoDB; 127 | - As notificações do prestador devem ser enviadas em tempo-real utilizando Socket.io; 128 | 129 | Regras de Negócio: 130 | [] A notificação deve ter um status de lida ou não-lida para que o prestador possa controlar; 131 | 132 | ### Agendamento de serviço 133 | 134 | Requisitos Funcionais: 135 | [] O usuário deve poder listar todos os prestadores de serviço cadastrados; 136 | [] O usuário deve poder visualizar os dias de um mês com pelo menos um horário disponível de um prestador; 137 | [] O usuário deve poder visualizar os horários disponíveis de um dia especifico de um prestador; 138 | [] O usuário deve poder realizar um novo agendamento com um prestador; 139 | [] O usuário deve poder listar os agendamentos já marcados; 140 | [] O usuário deve poder cancelar um agendamento marcado. 141 | 142 | Requisitos Não Funcionais: 143 | - A listagem de prestadores devem ser armazenadas em cache. 144 | 145 | Regras de Negócio: 146 | [] Cada agendamento deve durar 1h exatamente; 147 | [] Os agendamentos devem estar disponíveis entre 8h às 18h sendo o último agendamento iniciado as 17h; 148 | [] O usuário não pode agendar em um horário já ocupado; 149 | [] O usuário não pode agendar em um horário que já passou; 150 | [] O usuário não pode agendar consigo mesmo; 151 | 152 | --- 153 | 154 | ## :book: **Thiago Marinho** 155 | 156 | Desafio realizado por Thiago Marinho de Oliveira. 157 | 158 | ## tips/scripts 159 | 160 | ### criar migrations: 161 | 162 | - Tem um script no package para auxiliar nisso, uma vez que estamos usando ts. 163 | 164 | Terminal: `yarn typeorm migration:create -n CreateAppointments` 165 | 166 | - Execugtar migration: `yarn typeorm migration:run` 167 | - Rollback desfazer : `yarn typeorm migration:revert` 168 | 169 | Ver quais migrations já foram executadas: 170 | 171 | `yarn typeorm migration:show` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('./tsconfig.json'); 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/private/var/folders/4s/lh22vhl50wn202th6x39pt380000gn/T/jest_dx", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | // clearMocks: false, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | collectCoverage: true, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | collectCoverageFrom: ['/src/modules/**/services/*.ts'], 22 | 23 | // The directory where Jest should output its coverage files 24 | coverageDirectory: 'coverage', 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "/node_modules/" 29 | // ], 30 | 31 | // A list of reporter names that Jest uses when writing coverage reports 32 | coverageReporters: ['text-summary', 'lcov'], 33 | 34 | // An object that configures minimum threshold enforcement for coverage results 35 | // coverageThreshold: undefined, 36 | 37 | // A path to a custom dependency extractor 38 | // dependencyExtractor: undefined, 39 | 40 | // Make calling deprecated APIs throw helpful error messages 41 | // errorOnDeprecated: false, 42 | 43 | // Force coverage collection from ignored files using an array of glob patterns 44 | // forceCoverageMatch: [], 45 | 46 | // A path to a module which exports an async function that is triggered once before all test suites 47 | // globalSetup: undefined, 48 | 49 | // A path to a module which exports an async function that is triggered once after all test suites 50 | // globalTeardown: undefined, 51 | 52 | // A set of global variables that need to be available in all test environments 53 | // globals: {}, 54 | 55 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 56 | // maxWorkers: "50%", 57 | 58 | // An array of directory names to be searched recursively up from the requiring module's location 59 | // moduleDirectories: [ 60 | // "node_modules" 61 | // ], 62 | 63 | // An array of file extensions your modules use 64 | // moduleFileExtensions: [ 65 | // "js", 66 | // "json", 67 | // "jsx", 68 | // "ts", 69 | // "tsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 74 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { 75 | prefix: '/src/', 76 | }), 77 | 78 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 79 | // modulePathIgnorePatterns: [], 80 | 81 | // Activates notifications for test results 82 | // notify: false, 83 | 84 | // An enum that specifies notification mode. Requires { notify: true } 85 | // notifyMode: "failure-change", 86 | 87 | // A preset that is used as a base for Jest's configuration 88 | preset: 'ts-jest', 89 | 90 | // Run tests from one or more projects 91 | // projects: undefined, 92 | 93 | // Use this configuration option to add custom reporters to Jest 94 | // reporters: undefined, 95 | 96 | // Automatically reset mock state between every test 97 | // resetMocks: false, 98 | 99 | // Reset the module registry before running each individual test 100 | // resetModules: false, 101 | 102 | // A path to a custom resolver 103 | // resolver: undefined, 104 | 105 | // Automatically restore mock state between every test 106 | // restoreMocks: false, 107 | 108 | // The root directory that Jest should scan for tests and modules within 109 | // rootDir: undefined, 110 | 111 | // A list of paths to directories that Jest should use to search for files in 112 | // roots: [ 113 | // "" 114 | // ], 115 | 116 | // Allows you to use a custom runner instead of Jest's default test runner 117 | // runner: "jest-runner", 118 | 119 | // The paths to modules that run some code to configure or set up the testing environment before each test 120 | // setupFiles: [], 121 | 122 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 123 | // setupFilesAfterEnv: [], 124 | 125 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 126 | // snapshotSerializers: [], 127 | 128 | // The test environment that will be used for testing 129 | testEnvironment: 'node', 130 | 131 | // Options that will be passed to the testEnvironment 132 | // testEnvironmentOptions: {}, 133 | 134 | // Adds a location field to test results 135 | // testLocationInResults: false, 136 | 137 | // The glob patterns Jest uses to detect test files 138 | testMatch: ['**/*.spec.ts'], 139 | 140 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 141 | // testPathIgnorePatterns: [ 142 | // "/node_modules/" 143 | // ], 144 | 145 | // The regexp pattern or array of patterns that Jest uses to detect test files 146 | // testRegex: [], 147 | 148 | // This option allows the use of a custom results processor 149 | // testResultsProcessor: undefined, 150 | 151 | // This option allows use of a custom test runner 152 | // testRunner: "jasmine2", 153 | 154 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 155 | // testURL: "http://localhost", 156 | 157 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 158 | // timers: "real", 159 | 160 | // A map from regular expressions to paths to transformers 161 | // transform: undefined, 162 | 163 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 164 | // transformIgnorePatterns: [ 165 | // "/node_modules/" 166 | // ], 167 | 168 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 169 | // unmockedModulePathPatterns: undefined, 170 | 171 | // Indicates whether each individual test should be reported during the run 172 | // verbose: undefined, 173 | 174 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 175 | // watchPathIgnorePatterns: [], 176 | 177 | // Whether to use watchman for file crawling 178 | // watchman: true, 179 | }; 180 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primeiro-projeto-node", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "tsc", 8 | "dev:server": "ts-node-dev -r tsconfig-paths/register --inspect --transpileOnly --ignore-watch node_modules src/shared/infra/http/server.ts", 9 | "start": "ts-node src/shared/infra/http/server.ts", 10 | "typeorm": "ts-node-dev -r tsconfig-paths/register ./node_modules/typeorm/cli.js", 11 | "test": "jest" 12 | }, 13 | "dependencies": { 14 | "@types/cors": "^2.8.6", 15 | "@types/hapi__joi": "^17.1.0", 16 | "@types/ioredis": "^4.16.2", 17 | "aws-sdk": "^2.676.0", 18 | "bcryptjs": "^2.4.3", 19 | "celebrate": "^12.1.1", 20 | "class-transformer": "^0.2.3", 21 | "cors": "^2.8.5", 22 | "date-fns": "^2.12.0", 23 | "dotenv": "^8.2.0", 24 | "express": "^4.17.1", 25 | "express-async-errors": "^3.1.1", 26 | "handlebars": "^4.7.6", 27 | "ioredis": "^4.16.3", 28 | "jsonwebtoken": "^8.5.1", 29 | "mime": "^2.4.5", 30 | "mongodb": "^3.5.7", 31 | "multer": "^1.4.2", 32 | "nodemailer": "^6.4.6", 33 | "pg": "^8.0.2", 34 | "rate-limiter-flexible": "^2.1.4", 35 | "redis": "^3.0.2", 36 | "reflect-metadata": "^0.1.13", 37 | "tsyringe": "^4.1.0", 38 | "typeorm": "^0.2.24", 39 | "uuidv4": "^6.0.7" 40 | }, 41 | "devDependencies": { 42 | "@types/bcryptjs": "^2.4.2", 43 | "@types/express": "^4.17.6", 44 | "@types/jest": "^25.2.1", 45 | "@types/jsonwebtoken": "^8.3.9", 46 | "@types/mongodb": "^3.5.16", 47 | "@types/multer": "^1.4.2", 48 | "@types/nodemailer": "^6.4.0", 49 | "@types/redis": "^2.8.20", 50 | "@typescript-eslint/eslint-plugin": "^2.27.0", 51 | "@typescript-eslint/parser": "^2.27.1-alpha.18", 52 | "eslint": "^6.8.0", 53 | "eslint-config-airbnb-base": "^14.1.0", 54 | "eslint-config-prettier": "^6.10.1", 55 | "eslint-import-resolver-typescript": "^2.0.0", 56 | "eslint-plugin-import": "^2.20.1", 57 | "eslint-plugin-prettier": "^3.1.3", 58 | "jest": "^26.0.1", 59 | "prettier": "^2.0.4", 60 | "ts-jest": "^25.4.0", 61 | "ts-node-dev": "^1.0.0-pre.44", 62 | "tsconfig-paths": "^3.9.0", 63 | "typescript": "^3.8.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | arrowParens: 'avoid', 5 | }; 6 | -------------------------------------------------------------------------------- /src/@types/express.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user: { 4 | id: string; 5 | }; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/config/auth.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | jwt: { 3 | secret: process.env.APP_SECRET || 'default_test', 4 | expiresIn: '1d', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/config/cache.ts: -------------------------------------------------------------------------------- 1 | import { RedisOptions } from 'ioredis'; 2 | 3 | interface ICacheConfig { 4 | driver: 'redis'; 5 | config: { redis: RedisOptions }; 6 | } 7 | 8 | export default { 9 | driver: 'redis', 10 | 11 | config: { 12 | redis: { 13 | host: process.env.REDIS_HOST, 14 | port: process.env.REDIS_PORT, 15 | password: process.env.REDIS_PASS || undefined, 16 | }, 17 | }, 18 | } as ICacheConfig; 19 | -------------------------------------------------------------------------------- /src/config/mail.ts: -------------------------------------------------------------------------------- 1 | interface IMailConfig { 2 | driver: 'ethereal' | 'custom'; 3 | } 4 | 5 | export default { 6 | driver: process.env.MAIL_DRIVER || 'ethereal', 7 | } as IMailConfig; 8 | -------------------------------------------------------------------------------- /src/config/upload.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import multer, { StorageEngine } from 'multer'; 3 | import path from 'path'; 4 | 5 | const tempFolder = path.resolve(__dirname, '..', '..', 'temp'); 6 | 7 | interface IUploadConfig { 8 | driver: 's3' | 'disk'; 9 | 10 | tempFolder: string; 11 | uploadsFolder: string; 12 | 13 | multer: { 14 | storage: StorageEngine; 15 | }; 16 | config: { disk: {}; aws: { bucket: string } }; 17 | } 18 | 19 | export default { 20 | driver: process.env.STORAGE_DRIVER, 21 | 22 | tempFolder, 23 | uploadsFolder: path.resolve(tempFolder, 'uploads'), 24 | multer: { 25 | storage: multer.diskStorage({ 26 | destination: tempFolder, 27 | filename(request, file, callback) { 28 | const fileHash = crypto.randomBytes(10).toString('HEX'); 29 | const fileName = `${fileHash}-${file.originalname}`; 30 | 31 | return callback(null, fileName); 32 | }, 33 | }), 34 | }, 35 | config: { 36 | disk: {}, 37 | aws: { 38 | bucket: 'app-gobarber-2', 39 | }, 40 | }, 41 | } as IUploadConfig; 42 | -------------------------------------------------------------------------------- /src/modules/appointments/dtos/ICreateAppointmentDTO.ts: -------------------------------------------------------------------------------- 1 | export default interface ICreateAppointmentDTO { 2 | provider_id: string; 3 | user_id: string; 4 | date: Date; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/appointments/dtos/IFindAllInDayFromProviderDTO.ts: -------------------------------------------------------------------------------- 1 | export default interface IFindAllInDayFromProviderDTO { 2 | provider_id: string; 3 | month: number; 4 | year: number; 5 | day: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/appointments/dtos/IFindAllInMonthFromProviderDTO.ts: -------------------------------------------------------------------------------- 1 | export default interface IFindAllInMonthFromProviderDTO { 2 | provider_id: string; 3 | month: number; 4 | year: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/http/controllers/AppointmentsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { parseISO } from 'date-fns'; 3 | import { container } from 'tsyringe'; 4 | import CreateAppointmentService from '@modules/appointments/services/CreateAppointmentService'; 5 | 6 | export default class AppointmentsController { 7 | public async create(request: Request, response: Response): Promise { 8 | const user_id = request.user.id; 9 | const { provider_id, date } = request.body; 10 | 11 | const createAppointment = container.resolve(CreateAppointmentService); 12 | 13 | const appointment = await createAppointment.execute({ 14 | provider_id, 15 | user_id, 16 | date, 17 | }); 18 | 19 | return response.json(appointment); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/http/controllers/ProviderAppointmentsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | import ListProviderAppointmentsService from '@modules/appointments/services/ListProviderAppointmentsService'; 4 | 5 | export default class AppointmentsController { 6 | public async index(request: Request, response: Response): Promise { 7 | const provider_id = request.user.id; 8 | const { day, month, year } = request.body; 9 | 10 | const listProviderAppointments = container.resolve( 11 | ListProviderAppointmentsService, 12 | ); 13 | 14 | const appointments = await listProviderAppointments.execute({ 15 | provider_id, 16 | day, 17 | month, 18 | year, 19 | }); 20 | 21 | return response.json(appointments); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/http/controllers/ProviderDayAvailabilityController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | 4 | import ListProviderDayAvailabilityService from '@modules/appointments/services/ListProviderDayAvailabilityService'; 5 | 6 | export default class ProviderDayAvailabilityController { 7 | public async index(request: Request, response: Response): Promise { 8 | const { provider_id } = request.params; 9 | const { day, month, year } = request.body; 10 | 11 | const listProvidersDayAvailability = container.resolve( 12 | ListProviderDayAvailabilityService, 13 | ); 14 | 15 | const availability = await listProvidersDayAvailability.execute({ 16 | provider_id, 17 | day, 18 | month, 19 | year, 20 | }); 21 | 22 | return response.json(availability); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/http/controllers/ProviderMonthAvailabilityController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | 4 | import ListProviderMonthAvailabilityService from '@modules/appointments/services/ListProviderMonthAvailabilityService'; 5 | 6 | export default class ProviderMonthAvailabilityController { 7 | public async index(request: Request, response: Response): Promise { 8 | const { provider_id } = request.params; 9 | const { month, year } = request.body; 10 | 11 | const listProvidersMonthAvailability = container.resolve( 12 | ListProviderMonthAvailabilityService, 13 | ); 14 | 15 | const availability = await listProvidersMonthAvailability.execute({ 16 | provider_id, 17 | month, 18 | year, 19 | }); 20 | 21 | return response.json(availability); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/http/controllers/ProvidersController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | 4 | import ListProvidersService from '@modules/appointments/services/ListProvidersService'; 5 | 6 | export default class ProvidersController { 7 | public async index(request: Request, response: Response): Promise { 8 | const user_id = request.user.id; 9 | 10 | const listProviders = container.resolve(ListProvidersService); 11 | 12 | const providers = await listProviders.execute({ 13 | user_id, 14 | }); 15 | 16 | return response.json(providers); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/http/routes/appointments.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { celebrate, Joi, Segments } from 'celebrate'; 3 | import ensureAuthenticated from '@modules/users/infra/http/middlewares/ensureAuthenticated'; 4 | import AppointmentsController from '../controllers/AppointmentsController'; 5 | import ProviderAppointmentsController from '../controllers/ProviderAppointmentsController'; 6 | 7 | const appointmentsRouter = Router(); 8 | 9 | const appointmentsController = new AppointmentsController(); 10 | const providerAppointmentsController = new ProviderAppointmentsController(); 11 | 12 | appointmentsRouter.use(ensureAuthenticated); 13 | 14 | appointmentsRouter.post( 15 | '/', 16 | celebrate({ 17 | [Segments.BODY]: { 18 | provider_id: Joi.string().uuid().required(), 19 | date: Joi.date(), 20 | }, 21 | }), 22 | appointmentsController.create, 23 | ); 24 | appointmentsRouter.get('/me', providerAppointmentsController.index); 25 | 26 | export default appointmentsRouter; 27 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/http/routes/providers.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ensureAuthenticated from '@modules/users/infra/http/middlewares/ensureAuthenticated'; 3 | import { celebrate, Joi, Segments } from 'celebrate'; 4 | import ProvidersController from '../controllers/ProvidersController'; 5 | import ProviderDayAvailabilityController from '../controllers/ProviderDayAvailabilityController'; 6 | import ProviderMonthAvailabilityController from '../controllers/ProviderMonthAvailabilityController'; 7 | 8 | const providersRouter = Router(); 9 | 10 | const providersController = new ProvidersController(); 11 | const providerMonthAvailabilityController = new ProviderMonthAvailabilityController(); 12 | const providerDayAvailabilityController = new ProviderDayAvailabilityController(); 13 | 14 | providersRouter.use(ensureAuthenticated); 15 | 16 | providersRouter.get('/', providersController.index); 17 | 18 | providersRouter.get( 19 | '/:provider_id/month-availability', 20 | celebrate({ 21 | [Segments.PARAMS]: { 22 | provider_id: Joi.string().uuid().required(), 23 | }, 24 | }), 25 | providerMonthAvailabilityController.index, 26 | ); 27 | 28 | providersRouter.get( 29 | '/:provider_id/day-availability', 30 | celebrate({ 31 | [Segments.PARAMS]: { 32 | provider_id: Joi.string().uuid().required(), 33 | }, 34 | }), 35 | providerDayAvailabilityController.index, 36 | ); 37 | 38 | export default providersRouter; 39 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/typeorm/entities/Appointment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | ManyToOne, 8 | JoinColumn, 9 | } from 'typeorm'; 10 | 11 | import User from '@modules/users/infra/typeorm/entities/User'; 12 | 13 | @Entity('appointments') 14 | class Appointment { 15 | @PrimaryGeneratedColumn('uuid') 16 | id: string; 17 | 18 | @Column() 19 | provider_id: string; 20 | 21 | @ManyToOne(() => User) 22 | @JoinColumn({ name: 'provider_id' }) 23 | provider: User; 24 | 25 | @Column() 26 | user_id: string; 27 | 28 | @ManyToOne(() => User) 29 | @JoinColumn({ name: 'user_id' }) 30 | user: User; 31 | 32 | @Column('timestamp with time zone') 33 | date: Date; 34 | 35 | @CreateDateColumn() 36 | created_at: Date; 37 | 38 | @UpdateDateColumn() 39 | updated_at: Date; 40 | } 41 | 42 | export default Appointment; 43 | -------------------------------------------------------------------------------- /src/modules/appointments/infra/typeorm/repositories/AppointmentsRepository.ts: -------------------------------------------------------------------------------- 1 | import { getRepository, Repository, Raw } from 'typeorm'; 2 | import ICreateAppointmentDTO from '@modules/appointments/dtos/ICreateAppointmentDTO'; 3 | import IAppointmentsRepository from '@modules/appointments/repositories/IAppointmentsRepository'; 4 | import IFindAllInMonthFromProviderDTO from '@modules/appointments/dtos/IFindAllInMonthFromProviderDTO'; 5 | import IFindAllInDayFromProviderDTO from '@modules/appointments/dtos/IFindAllInDayFromProviderDTO'; 6 | import Appointment from '../entities/Appointment'; 7 | 8 | class AppointmentRepository implements IAppointmentsRepository { 9 | private ormRepository: Repository; 10 | 11 | constructor() { 12 | this.ormRepository = getRepository(Appointment); 13 | } 14 | 15 | public async findAllInMonthFromProvider({ 16 | provider_id, 17 | month, 18 | year, 19 | }: IFindAllInMonthFromProviderDTO): Promise { 20 | const parsedMonth = String(month).padStart(2, '0'); 21 | 22 | const appointments = await this.ormRepository.find({ 23 | where: { 24 | provider_id, 25 | date: Raw( 26 | dateFieldName => 27 | `to_char(${dateFieldName}, 'MM-YYYY') = '${parsedMonth}-${year}'`, 28 | ), 29 | }, 30 | }); 31 | 32 | return appointments; 33 | } 34 | 35 | public async findAllInDayFromProvider({ 36 | provider_id, 37 | day, 38 | month, 39 | year, 40 | }: IFindAllInDayFromProviderDTO): Promise { 41 | const parsedDay = String(day).padStart(2, '0'); 42 | const parsedMonth = String(month).padStart(2, '0'); 43 | 44 | const appointments = await this.ormRepository.find({ 45 | where: { 46 | provider_id, 47 | date: Raw( 48 | dateFieldName => 49 | `to_char(${dateFieldName}, 'DD-MM-YYYY') = '${parsedDay}-${parsedMonth}-${year}'`, 50 | ), 51 | }, 52 | }); 53 | 54 | return appointments; 55 | } 56 | 57 | public async findByDate(date: Date): Promise { 58 | const findAppointment = await this.ormRepository.findOne({ 59 | where: { date }, 60 | }); 61 | 62 | return findAppointment; 63 | } 64 | 65 | public async create({ 66 | provider_id, 67 | user_id, 68 | date, 69 | }: ICreateAppointmentDTO): Promise { 70 | const appointment = this.ormRepository.create({ 71 | provider_id, 72 | user_id, 73 | date, 74 | }); 75 | 76 | await this.ormRepository.save(appointment); 77 | 78 | return appointment; 79 | } 80 | } 81 | 82 | export default AppointmentRepository; 83 | -------------------------------------------------------------------------------- /src/modules/appointments/repositories/IAppointmentsRepository.ts: -------------------------------------------------------------------------------- 1 | import Appointment from '../infra/typeorm/entities/Appointment'; 2 | import ICreateAppointmentDTO from '../dtos/ICreateAppointmentDTO'; 3 | import IFindAllInMonthFromProviderDTO from '../dtos/IFindAllInMonthFromProviderDTO'; 4 | import IFindAllInDayFromProviderDTO from '../dtos/IFindAllInDayFromProviderDTO'; 5 | 6 | export default interface IAppointmentsRepository { 7 | create(data: ICreateAppointmentDTO): Promise; 8 | findByDate(date: Date): Promise; 9 | findAllInMonthFromProvider( 10 | data: IFindAllInMonthFromProviderDTO, 11 | ): Promise; 12 | findAllInDayFromProvider( 13 | data: IFindAllInDayFromProviderDTO, 14 | ): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/appointments/repositories/fakes/FakeAppointmentsRepository.ts: -------------------------------------------------------------------------------- 1 | import ICreateAppointmentDTO from '@modules/appointments/dtos/ICreateAppointmentDTO'; 2 | import IAppointmentsRepository from '@modules/appointments/repositories/IAppointmentsRepository'; 3 | import IFindAllInMonthFromProviderDTO from '@modules/appointments/dtos/IFindAllInMonthFromProviderDTO'; 4 | import IFindAllInDayFromProviderDTO from '@modules/appointments/dtos/IFindAllInDayFromProviderDTO'; 5 | import { uuid } from 'uuidv4'; 6 | import { isEqual, getMonth, getYear, getDate } from 'date-fns'; 7 | import Appointment from '../../infra/typeorm/entities/Appointment'; 8 | 9 | class FakeAppointmentRepository implements IAppointmentsRepository { 10 | private appointments: Appointment[] = []; 11 | 12 | public async findAllInMonthFromProvider({ 13 | provider_id, 14 | month, 15 | year, 16 | }: IFindAllInMonthFromProviderDTO): Promise { 17 | const appointments = this.appointments.filter(appointment => { 18 | return ( 19 | appointment.provider_id === provider_id && 20 | getMonth(appointment.date) + 1 === month && 21 | getYear(appointment.date) === year 22 | ); 23 | }); 24 | 25 | return appointments; 26 | } 27 | 28 | public async findAllInDayFromProvider({ 29 | provider_id, 30 | month, 31 | year, 32 | day, 33 | }: IFindAllInDayFromProviderDTO): Promise { 34 | const appointments = this.appointments.filter(appointment => { 35 | return ( 36 | appointment.provider_id === provider_id && 37 | getDate(appointment.date) === day && 38 | getMonth(appointment.date) + 1 === month && 39 | getYear(appointment.date) === year 40 | ); 41 | }); 42 | 43 | return appointments; 44 | } 45 | 46 | public async findByDate(date: Date): Promise { 47 | const findAppointment = this.appointments.find(appointment => 48 | isEqual(appointment.date, date), 49 | ); 50 | 51 | return findAppointment; 52 | } 53 | 54 | public async create({ 55 | provider_id, 56 | user_id, 57 | date, 58 | }: ICreateAppointmentDTO): Promise { 59 | const appointment = new Appointment(); 60 | // appointment.id = uuid(); 61 | // appointment.date = date; 62 | // appointment.provider_id = provider_id; 63 | Object.assign(appointment, { id: uuid(), date, user_id, provider_id }); 64 | 65 | this.appointments.push(appointment); 66 | 67 | return appointment; 68 | } 69 | } 70 | 71 | export default FakeAppointmentRepository; 72 | -------------------------------------------------------------------------------- /src/modules/appointments/services/CreateAppointmentService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeNotificationsRepository from '@modules/notifications/repositories/fakes/FakeNotificationsRepository'; 3 | import FakeCacheProvider from '@shared/container/providers/CacheProvider/implementations/fakes/FakeCacheProvider'; 4 | import FakeAppointmentsRepository from '../repositories/fakes/FakeAppointmentsRepository'; 5 | import CreateAppointmentService from './CreateAppointmentService'; 6 | 7 | describe('CreateAppointment', () => { 8 | let fakeAppointmentsRepository: FakeAppointmentsRepository; 9 | let fakeNotificationsRepository: FakeNotificationsRepository; 10 | let createAppointment: CreateAppointmentService; 11 | let fakeCacheProvider: FakeCacheProvider; 12 | 13 | beforeEach(() => { 14 | fakeAppointmentsRepository = new FakeAppointmentsRepository(); 15 | fakeNotificationsRepository = new FakeNotificationsRepository(); 16 | fakeCacheProvider = new FakeCacheProvider(); 17 | createAppointment = new CreateAppointmentService( 18 | fakeAppointmentsRepository, 19 | fakeNotificationsRepository, 20 | fakeCacheProvider, 21 | ); 22 | }); 23 | 24 | it('should be able to create a new appointment', async () => { 25 | jest.spyOn(Date, 'now').mockImplementationOnce(() => { 26 | return new Date(2020, 4, 10, 12).getTime(); 27 | }); 28 | 29 | const appointment = await createAppointment.execute({ 30 | date: new Date(2020, 4, 10, 13), 31 | user_id: 'userLogged', 32 | provider_id: '12321312', 33 | }); 34 | 35 | expect(appointment).toHaveProperty('id'); 36 | expect(appointment.provider_id).toBe('12321312'); 37 | }); 38 | 39 | it('should not be able to create two appointment on the same time', async () => { 40 | const appointmentDate = new Date(2060, 4, 12, 11); 41 | 42 | await createAppointment.execute({ 43 | date: appointmentDate, 44 | user_id: 'userLogged', 45 | provider_id: '12321312', 46 | }); 47 | 48 | await expect( 49 | createAppointment.execute({ 50 | date: appointmentDate, 51 | user_id: 'userLogged', 52 | provider_id: '12321312', 53 | }), 54 | ).rejects.toBeInstanceOf(AppError); 55 | }); 56 | 57 | it('should not be able to create an appointment on a past date', async () => { 58 | jest.spyOn(Date, 'now').mockImplementationOnce(() => { 59 | return new Date(2020, 4, 10, 12).getTime(); 60 | }); 61 | 62 | await expect( 63 | createAppointment.execute({ 64 | date: new Date(2020, 4, 10, 11), 65 | user_id: 'userLogged', 66 | provider_id: '12321312', 67 | }), 68 | ).rejects.toBeInstanceOf(AppError); 69 | }); 70 | 71 | it('should not be able to create an appointment with same user as proider', async () => { 72 | jest.spyOn(Date, 'now').mockImplementationOnce(() => { 73 | return new Date(2020, 4, 10, 12).getTime(); 74 | }); 75 | 76 | await expect( 77 | createAppointment.execute({ 78 | date: new Date(2020, 4, 10, 13), 79 | user_id: 'userLogged', 80 | provider_id: 'userLogged', 81 | }), 82 | ).rejects.toBeInstanceOf(AppError); 83 | }); 84 | 85 | it('should not be able to create an appointment before 8am and after 5pm.', async () => { 86 | jest.spyOn(Date, 'now').mockImplementationOnce(() => { 87 | return new Date(2020, 4, 10, 12).getTime(); 88 | }); 89 | 90 | await expect( 91 | createAppointment.execute({ 92 | date: new Date(2020, 4, 11, 7), 93 | user_id: 'userLogged', 94 | provider_id: 'provider-id', 95 | }), 96 | ).rejects.toBeInstanceOf(AppError); 97 | 98 | await expect( 99 | createAppointment.execute({ 100 | date: new Date(2020, 4, 11, 18), 101 | user_id: 'userLogged', 102 | provider_id: 'provider-id', 103 | }), 104 | ).rejects.toBeInstanceOf(AppError); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/modules/appointments/services/CreateAppointmentService.ts: -------------------------------------------------------------------------------- 1 | import { startOfHour, isBefore, getHours, format } from 'date-fns'; 2 | import { injectable, inject } from 'tsyringe'; 3 | import AppError from '@shared/errors/AppError'; 4 | import INotificationsRepository from '@modules/notifications/repositories/INotificationsRepository'; 5 | import ICacheProvider from '@shared/container/providers/CacheProvider/models/ICacheProvider'; 6 | import Appointment from '../infra/typeorm/entities/Appointment'; 7 | import IAppointmentsRepository from '../repositories/IAppointmentsRepository'; 8 | 9 | interface IRequest { 10 | provider_id: string; 11 | user_id: string; 12 | date: Date; 13 | } 14 | 15 | @injectable() 16 | class CreateAppointmentService { 17 | constructor( 18 | @inject('AppointmentsRepository') 19 | private appointmentsRepository: IAppointmentsRepository, 20 | 21 | @inject('NotificationsRepository') 22 | private notificationsRepository: INotificationsRepository, 23 | 24 | @inject('CacheProvider') 25 | private cacheProvider: ICacheProvider, 26 | ) {} 27 | 28 | public async execute({ 29 | provider_id, 30 | user_id, 31 | date, 32 | }: IRequest): Promise { 33 | const appointmentDate = startOfHour(date); 34 | 35 | if (user_id === provider_id) { 36 | throw new AppError('You cannot create an appointment with yourself'); 37 | } 38 | 39 | const hour = getHours(appointmentDate); 40 | if (hour < 8 || hour > 17) { 41 | throw new AppError( 42 | 'You can only create appointments between 8am and 5pm.', 43 | ); 44 | } 45 | 46 | if (isBefore(appointmentDate, Date.now())) { 47 | throw new AppError('you cannot create an appointment in the past date'); 48 | } 49 | 50 | const findAppointmentInSameDate = await this.appointmentsRepository.findByDate( 51 | appointmentDate, 52 | ); 53 | 54 | if (findAppointmentInSameDate) { 55 | throw new AppError('This appointment is already booked'); 56 | } 57 | 58 | const appointment = await this.appointmentsRepository.create({ 59 | provider_id, 60 | user_id, 61 | date: appointmentDate, 62 | }); 63 | 64 | const dateFormatted = format(appointmentDate, "dd/MM/yyyy 'às' HH:mm"); 65 | 66 | await this.notificationsRepository.create({ 67 | recipient_id: provider_id, 68 | content: `Novo agendamento para ${dateFormatted}`, 69 | }); 70 | 71 | await this.cacheProvider.invalidate( 72 | `provider-appointments:${provider_id}:${format( 73 | appointmentDate, 74 | 'yyyy-M-d', 75 | )}`, 76 | ); 77 | 78 | return appointment; 79 | } 80 | } 81 | 82 | export default CreateAppointmentService; 83 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProviderAppointmentsService.spec.ts: -------------------------------------------------------------------------------- 1 | import FakeCacheProvider from '@shared/container/providers/CacheProvider/implementations/fakes/FakeCacheProvider'; 2 | import FakeAppointmentsRepository from '../repositories/fakes/FakeAppointmentsRepository'; 3 | import ListProviderAppointmentsService from './ListProviderAppointmentsService'; 4 | 5 | let fakeAppointmentsRepository: FakeAppointmentsRepository; 6 | let listProviderAppointments: ListProviderAppointmentsService; 7 | let fakeCacheProvider: FakeCacheProvider; 8 | 9 | describe('ListProviderAppointmentsService', () => { 10 | beforeEach(() => { 11 | fakeAppointmentsRepository = new FakeAppointmentsRepository(); 12 | fakeCacheProvider = new FakeCacheProvider(); 13 | listProviderAppointments = new ListProviderAppointmentsService( 14 | fakeAppointmentsRepository, 15 | fakeCacheProvider, 16 | ); 17 | }); 18 | 19 | it('should be able to list the appointments on a specific day', async () => { 20 | const appointment1 = await fakeAppointmentsRepository.create({ 21 | provider_id: 'provider', 22 | user_id: 'user', 23 | date: new Date(2020, 4, 20, 14, 0, 0), 24 | }); 25 | 26 | const appointment2 = await fakeAppointmentsRepository.create({ 27 | provider_id: 'provider', 28 | user_id: 'user', 29 | date: new Date(2020, 4, 20, 15, 0, 0), 30 | }); 31 | 32 | const appointments = await listProviderAppointments.execute({ 33 | provider_id: 'provider', 34 | day: 20, 35 | month: 5, 36 | year: 2020, 37 | }); 38 | 39 | await expect(appointments).toEqual([appointment1, appointment2]); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProviderAppointmentsService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'tsyringe'; 2 | import Appointment from '@modules/appointments/infra/typeorm/entities/Appointment'; 3 | import ICacheProvider from '@shared/container/providers/CacheProvider/models/ICacheProvider'; 4 | import IAppointmentsRepository from '../repositories/IAppointmentsRepository'; 5 | 6 | interface IRequest { 7 | provider_id: string; 8 | day: number; 9 | month: number; 10 | year: number; 11 | } 12 | 13 | @injectable() 14 | class ListProviderAppointmentsService { 15 | constructor( 16 | @inject('AppointmentsRepository') 17 | private appointmentsRepository: IAppointmentsRepository, 18 | 19 | @inject('CacheProvider') 20 | private cacheProvider: ICacheProvider, 21 | ) {} 22 | 23 | public async execute({ 24 | provider_id, 25 | day, 26 | month, 27 | year, 28 | }: IRequest): Promise { 29 | const cacheKey = `provider-appointments:${provider_id}:${year}-${month}-${day}`; 30 | 31 | let appointments = await this.cacheProvider.recover( 32 | cacheKey, 33 | ); 34 | 35 | if (!appointments) { 36 | appointments = await this.appointmentsRepository.findAllInDayFromProvider( 37 | { 38 | provider_id, 39 | day, 40 | month, 41 | year, 42 | }, 43 | ); 44 | 45 | await this.cacheProvider.save(cacheKey, appointments); 46 | } 47 | 48 | return appointments; 49 | } 50 | } 51 | 52 | export default ListProviderAppointmentsService; 53 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProviderDayAvailabilityService.spec.ts: -------------------------------------------------------------------------------- 1 | import FakeAppointmentsRepository from '../repositories/fakes/FakeAppointmentsRepository'; 2 | import ListProviderDayAvailabilityService from './ListProviderDayAvailabilityService'; 3 | 4 | let fakeAppointmentsRepository: FakeAppointmentsRepository; 5 | let listProviderDayAvailabity: ListProviderDayAvailabilityService; 6 | 7 | describe('ListProviderDayAvailabilityService', () => { 8 | beforeEach(() => { 9 | fakeAppointmentsRepository = new FakeAppointmentsRepository(); 10 | listProviderDayAvailabity = new ListProviderDayAvailabilityService( 11 | fakeAppointmentsRepository, 12 | ); 13 | }); 14 | 15 | it('should be able to list availability of the day from provider', async () => { 16 | await fakeAppointmentsRepository.create({ 17 | provider_id: 'user', 18 | user_id: 'client', 19 | date: new Date(2020, 4, 20, 14, 0, 0), 20 | }); 21 | 22 | await fakeAppointmentsRepository.create({ 23 | provider_id: 'user', 24 | user_id: 'client', 25 | date: new Date(2020, 4, 20, 15, 0, 0), 26 | }); 27 | 28 | jest.spyOn(Date, 'now').mockImplementationOnce(() => { 29 | return new Date(2020, 4, 20, 11).getTime(); 30 | }); 31 | 32 | const availability = await listProviderDayAvailabity.execute({ 33 | provider_id: 'user', 34 | day: 20, 35 | month: 5, 36 | year: 2020, 37 | }); 38 | 39 | await expect(availability).toEqual( 40 | expect.arrayContaining([ 41 | { 42 | hour: 8, 43 | available: false, 44 | }, 45 | { 46 | hour: 9, 47 | available: false, 48 | }, 49 | { 50 | hour: 10, 51 | available: false, 52 | }, 53 | { 54 | hour: 11, // hora que está verificando o a disponibilidade do provider 55 | available: false, 56 | }, 57 | { 58 | hour: 13, 59 | available: true, 60 | }, 61 | { 62 | hour: 14, 63 | available: false, 64 | }, 65 | { 66 | hour: 15, 67 | available: false, 68 | }, 69 | { 70 | hour: 16, 71 | available: true, 72 | }, 73 | ]), 74 | ); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProviderDayAvailabilityService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'tsyringe'; 2 | import { getHours, isAfter } from 'date-fns'; 3 | import IAppointmentsRepository from '../repositories/IAppointmentsRepository'; 4 | 5 | interface IRequest { 6 | provider_id: string; 7 | month: number; 8 | year: number; 9 | day: number; 10 | } 11 | 12 | type IResponse = Array<{ 13 | hour: number; 14 | available: boolean; 15 | }>; 16 | 17 | @injectable() 18 | class ListProviderDayAvailabilityService { 19 | constructor( 20 | @inject('AppointmentsRepository') 21 | private appointmentsRepository: IAppointmentsRepository, 22 | ) {} 23 | 24 | public async execute({ 25 | provider_id, 26 | day, 27 | month, 28 | year, 29 | }: IRequest): Promise { 30 | const appointments = await this.appointmentsRepository.findAllInDayFromProvider( 31 | { 32 | provider_id, 33 | day, 34 | month, 35 | year, 36 | }, 37 | ); 38 | 39 | const hourStar = 8; 40 | 41 | const eachHourArray = Array.from( 42 | { length: 10 }, 43 | (_, index) => index + hourStar, 44 | ); 45 | 46 | const currentDate = new Date(Date.now()); 47 | 48 | const availability = eachHourArray.map(hour => { 49 | const hasAppointmentInHour = appointments.find( 50 | appointment => getHours(appointment.date) === hour, 51 | ); 52 | 53 | const compareDate = new Date(year, month - 1, day, hour); 54 | 55 | return { 56 | hour, 57 | available: !hasAppointmentInHour && isAfter(compareDate, currentDate), 58 | }; 59 | }); 60 | 61 | return availability; 62 | } 63 | } 64 | 65 | export default ListProviderDayAvailabilityService; 66 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProviderMonthAvailabilityService.spec.ts: -------------------------------------------------------------------------------- 1 | import FakeAppointmentsRepository from '../repositories/fakes/FakeAppointmentsRepository'; 2 | import ListProviderMonthAvailabilityService from './ListProviderMonthAvailabilityService'; 3 | 4 | let fakeAppointmentsRepository: FakeAppointmentsRepository; 5 | let listProviderMonthAvailabity: ListProviderMonthAvailabilityService; 6 | 7 | describe('ListProviderMonthAvailabilityService', () => { 8 | beforeEach(() => { 9 | fakeAppointmentsRepository = new FakeAppointmentsRepository(); 10 | listProviderMonthAvailabity = new ListProviderMonthAvailabilityService( 11 | fakeAppointmentsRepository, 12 | ); 13 | }); 14 | 15 | it('should be able to list providers availables in the month', async () => { 16 | const promissesCreateAppointmentInBusinessHours = Array.from( 17 | { length: 10 }, 18 | (_, index) => { 19 | return fakeAppointmentsRepository.create({ 20 | provider_id: 'provider', 21 | user_id: 'loogedUser', 22 | date: new Date(2020, 3, 20, index + 8, 0, 0), 23 | }); 24 | }, 25 | ); 26 | 27 | await Promise.all(promissesCreateAppointmentInBusinessHours); 28 | 29 | await fakeAppointmentsRepository.create({ 30 | provider_id: 'provider', 31 | user_id: 'loogedUser', 32 | date: new Date(2020, 3, 21, 8, 0, 0), 33 | }); 34 | 35 | const availability = await listProviderMonthAvailabity.execute({ 36 | provider_id: 'provider', 37 | year: 2020, 38 | month: 4, 39 | }); 40 | 41 | await expect(availability).toEqual( 42 | expect.arrayContaining([ 43 | { 44 | day: 19, 45 | available: true, 46 | }, 47 | { 48 | day: 20, 49 | available: false, 50 | }, 51 | { 52 | day: 21, 53 | available: true, 54 | }, 55 | { 56 | day: 22, 57 | available: true, 58 | }, 59 | ]), 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProviderMonthAvailabilityService.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from 'tsyringe'; 2 | import { getDaysInMonth, getDate } from 'date-fns'; 3 | import IAppointmentsRepository from '../repositories/IAppointmentsRepository'; 4 | 5 | interface IRequest { 6 | provider_id: string; 7 | month: number; 8 | year: number; 9 | } 10 | 11 | type IResponse = Array<{ 12 | day: number; 13 | available: boolean; 14 | }>; 15 | 16 | @injectable() 17 | class ListProviderMonthAvailabilityService { 18 | constructor( 19 | @inject('AppointmentsRepository') 20 | private appointmentsRepository: IAppointmentsRepository, 21 | ) {} 22 | 23 | public async execute({ 24 | provider_id, 25 | month, 26 | year, 27 | }: IRequest): Promise { 28 | const appointments = await this.appointmentsRepository.findAllInMonthFromProvider( 29 | { provider_id, month, year }, 30 | ); 31 | 32 | const numberOfDaysInMonth = getDaysInMonth(new Date(year, month - 1)); 33 | 34 | const eachDayArray = Array.from( 35 | { length: numberOfDaysInMonth }, 36 | (_, index) => index + 1, 37 | ); 38 | 39 | const availability = eachDayArray.map(day => { 40 | const appointmentsInDay = appointments.filter(appointment => { 41 | return getDate(appointment.date) === day; 42 | }); 43 | 44 | return { day, available: appointmentsInDay.length < 10 }; 45 | }); 46 | 47 | return availability; 48 | } 49 | } 50 | 51 | export default ListProviderMonthAvailabilityService; 52 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProvidersService.spec.ts: -------------------------------------------------------------------------------- 1 | import FakeUsersRepository from '@modules/users/repositories/fakes/FakeUsersRepository'; 2 | import FakeCacheProvider from '@shared/container/providers/CacheProvider/implementations/fakes/FakeCacheProvider'; 3 | import ListProvidersService from './ListProvidersService'; 4 | 5 | describe('ListProviderService', () => { 6 | let fakeUsersRepository: FakeUsersRepository; 7 | let listProviders: ListProvidersService; 8 | let fakeCacheProvider: FakeCacheProvider; 9 | 10 | beforeEach(() => { 11 | fakeUsersRepository = new FakeUsersRepository(); 12 | fakeCacheProvider = new FakeCacheProvider(); 13 | 14 | listProviders = new ListProvidersService( 15 | fakeUsersRepository, 16 | fakeCacheProvider, 17 | ); 18 | }); 19 | 20 | it('should be able to list the providers', async () => { 21 | const user1 = await fakeUsersRepository.create({ 22 | name: 'Thiago Marinho', 23 | email: 'tgmarinho@gmail.com', 24 | password: '123456', 25 | }); 26 | 27 | const user2 = await fakeUsersRepository.create({ 28 | name: 'Diego Fernandes', 29 | email: 'diegofernandes@gmail.com', 30 | password: '123456', 31 | }); 32 | 33 | const loggedUser = await fakeUsersRepository.create({ 34 | name: 'Usuário logado', 35 | email: 'ulogged@gmail.com', 36 | password: '123456', 37 | }); 38 | 39 | const providers = await listProviders.execute({ 40 | user_id: loggedUser.id, 41 | }); 42 | 43 | await expect(providers).toEqual([user1, user2]); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/modules/appointments/services/ListProvidersService.ts: -------------------------------------------------------------------------------- 1 | import User from '@modules/users/infra/typeorm/entities/User'; 2 | import { injectable, inject } from 'tsyringe'; 3 | import IUsersRepository from '@modules/users/repositories/IUsersRepository'; 4 | import ICacheProvider from '@shared/container/providers/CacheProvider/models/ICacheProvider'; 5 | 6 | interface IRequest { 7 | user_id: string; 8 | } 9 | 10 | @injectable() 11 | class ListProvidersService { 12 | constructor( 13 | @inject('UsersRepository') 14 | private usersRepository: IUsersRepository, 15 | 16 | @inject('CacheProvider') 17 | private cacheProvider: ICacheProvider, 18 | ) {} 19 | 20 | public async execute({ user_id }: IRequest): Promise { 21 | let users = await this.cacheProvider.recover( 22 | `providers-list:${user_id}`, 23 | ); 24 | 25 | if (!users) { 26 | users = await this.usersRepository.findAllProviders({ 27 | except_user_id: user_id, 28 | }); 29 | 30 | await this.cacheProvider.save(`providers-list:${user_id}`, users); 31 | } 32 | 33 | return users; 34 | } 35 | } 36 | 37 | export default ListProvidersService; 38 | -------------------------------------------------------------------------------- /src/modules/notifications/dtos/ICreateNotificationDTO.ts: -------------------------------------------------------------------------------- 1 | export default interface ICreateNotificationDTO { 2 | content: string; 3 | recipient_id: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/notifications/infra/typeorm/repositories/NotificationsRepository.ts: -------------------------------------------------------------------------------- 1 | import { getMongoRepository, MongoRepository } from 'typeorm'; 2 | import ICreateNotificationDTO from '@modules/notifications/dtos/ICreateNotificationDTO'; 3 | import INotificationsRepository from '@modules/notifications/repositories/INotificationsRepository'; 4 | import Notification from '../schemas/Notification'; 5 | 6 | class NotificationsRepository implements INotificationsRepository { 7 | private ormRepository: MongoRepository; 8 | 9 | constructor() { 10 | this.ormRepository = getMongoRepository(Notification, 'mongo'); 11 | } 12 | 13 | public async create({ 14 | content, 15 | recipient_id, 16 | }: ICreateNotificationDTO): Promise { 17 | const notification = this.ormRepository.create({ 18 | content, 19 | recipient_id, 20 | }); 21 | 22 | await this.ormRepository.save(notification); 23 | 24 | return notification; 25 | } 26 | } 27 | 28 | export default NotificationsRepository; 29 | -------------------------------------------------------------------------------- /src/modules/notifications/infra/typeorm/schemas/Notification.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectID, 3 | Entity, 4 | Column, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | ObjectIdColumn, 8 | } from 'typeorm'; 9 | 10 | @Entity('notifications') 11 | class Notification { 12 | @ObjectIdColumn() 13 | id: ObjectID; 14 | 15 | @Column() 16 | content: string; 17 | 18 | @Column('uuid') 19 | recipient_id: string; 20 | 21 | @Column({ default: false }) 22 | read: boolean; 23 | 24 | @CreateDateColumn() 25 | created_at: Date; 26 | 27 | @UpdateDateColumn() 28 | update_at: Date; 29 | } 30 | 31 | export default Notification; 32 | -------------------------------------------------------------------------------- /src/modules/notifications/repositories/INotificationsRepository.ts: -------------------------------------------------------------------------------- 1 | import ICreateNotificationDTO from '../dtos/ICreateNotificationDTO'; 2 | import Notification from '../infra/typeorm/schemas/Notification'; 3 | 4 | export default interface INotificationsRepository { 5 | create(data: ICreateNotificationDTO): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/notifications/repositories/fakes/FakeNotificationsRepository.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from 'mongodb'; 2 | import ICreateNotificationDTO from '@modules/notifications/dtos/ICreateNotificationDTO'; 3 | import INotificationsRepository from '@modules/notifications/repositories/INotificationsRepository'; 4 | import Notification from '../../infra/typeorm/schemas/Notification'; 5 | 6 | class NotificationsRepository implements INotificationsRepository { 7 | private notifications: Notification[] = []; 8 | 9 | public async create({ 10 | content, 11 | recipient_id, 12 | }: ICreateNotificationDTO): Promise { 13 | const notification = new Notification(); 14 | 15 | Object.assign(notification, { id: new ObjectID(), content, recipient_id }); 16 | 17 | this.notifications.push(notification); 18 | 19 | return notification; 20 | } 21 | } 22 | 23 | export default NotificationsRepository; 24 | -------------------------------------------------------------------------------- /src/modules/users/dtos/ICreateUserDTO.ts: -------------------------------------------------------------------------------- 1 | export default interface ICreateUserDTO { 2 | name: string; 3 | email: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/users/dtos/IFindAllProvidersDTO.ts: -------------------------------------------------------------------------------- 1 | export default interface IFindAllProvidersDTO { 2 | except_user_id: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/controllers/ForgotPasswordController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | import SendForgotPasswordEmailService from '@modules/users/services/SendForgotPasswordEmailService'; 4 | 5 | export default class ForgotPasswordController { 6 | public async create(request: Request, response: Response): Promise { 7 | const { email } = request.body; 8 | 9 | const sendForgotPasswordEmail = container.resolve( 10 | SendForgotPasswordEmailService, 11 | ); 12 | 13 | await sendForgotPasswordEmail.execute({ 14 | email, 15 | }); 16 | 17 | return response.status(204).json(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/controllers/ProfileController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | import UpdateProfileService from '@modules/users/services/UpdateProfileService'; 4 | import ShowProfileService from '@modules/users/services/ShowProfileService'; 5 | import { classToClass } from 'class-transformer'; 6 | 7 | export default class ProfileController { 8 | public async show(request: Request, response: Response): Promise { 9 | const user_id = request.user.id; 10 | const showProfile = container.resolve(ShowProfileService); 11 | 12 | const user = await showProfile.execute({ user_id }); 13 | 14 | return response.json(classToClass(user)); 15 | } 16 | 17 | public async update(request: Request, response: Response): Promise { 18 | const user_id = request.user.id; 19 | const { name, email, old_password, password } = request.body; 20 | 21 | const updateProfile = container.resolve(UpdateProfileService); 22 | 23 | const user = await updateProfile.execute({ 24 | user_id, 25 | name, 26 | email, 27 | old_password, 28 | password, 29 | }); 30 | 31 | return response.json(classToClass(user)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/controllers/ResetPasswordController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | import ResetPasswordService from '@modules/users/services/ResetPasswordService'; 4 | 5 | export default class ResetPasswordController { 6 | public async create(request: Request, response: Response): Promise { 7 | const { password, token } = request.body; 8 | 9 | const resetPasswordService = container.resolve(ResetPasswordService); 10 | 11 | await resetPasswordService.execute({ 12 | token, 13 | password, 14 | }); 15 | 16 | return response.status(204).json(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/controllers/SessionsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | import AuthenticateUserService from '@modules/users/services/AuthenticateUserService'; 4 | import { classToClass } from 'class-transformer'; 5 | 6 | export default class SessionsController { 7 | public async create(request: Request, response: Response): Promise { 8 | const { email, password } = request.body; 9 | 10 | const authenticateUserService = container.resolve(AuthenticateUserService); 11 | 12 | const { user, token } = await authenticateUserService.execute({ 13 | email, 14 | password, 15 | }); 16 | 17 | return response.json({ user: classToClass(user), token }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/controllers/UserAvatarController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | import UpdateUserAvatarService from '@modules/users/services/UpdateUserAvatarService'; 4 | import { classToClass } from 'class-transformer'; 5 | 6 | export default class UserAvatarControllerController { 7 | public async update(request: Request, response: Response): Promise { 8 | const updateUserAvatar = container.resolve(UpdateUserAvatarService); 9 | 10 | const user = await updateUserAvatar.execute({ 11 | user_id: request.user.id, 12 | avatarFilename: request.file.filename, 13 | }); 14 | 15 | return response.json(classToClass(user)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/controllers/UsersController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { container } from 'tsyringe'; 3 | import CreateUserService from '@modules/users/services/CreateUserService'; 4 | import { classToClass } from 'class-transformer'; 5 | 6 | export default class UsersController { 7 | public async create(request: Request, response: Response): Promise { 8 | const { name, email, password } = request.body; 9 | 10 | const createUser = container.resolve(CreateUserService); 11 | 12 | const user = await createUser.execute({ name, email, password }); 13 | 14 | return response.json(classToClass(user)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/middlewares/ensureAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { verify } from 'jsonwebtoken'; 3 | import authConfig from '@config/auth'; 4 | import AppError from '@shared/errors/AppError'; 5 | 6 | interface ITokenPayload { 7 | iat: number; 8 | exp: number; 9 | sub: string; 10 | } 11 | 12 | export default function ensureAuthenticated( 13 | request: Request, 14 | response: Response, 15 | next: NextFunction, 16 | ): void { 17 | const authHeader = request.headers.authorization; 18 | 19 | if (!authHeader) { 20 | throw new AppError('JTW token is missing', 401); 21 | } 22 | 23 | const [, token] = authHeader.split(' '); 24 | try { 25 | const decoded = verify(token, authConfig.jwt.secret); 26 | 27 | const { sub } = decoded as ITokenPayload; 28 | 29 | request.user = { 30 | id: sub, 31 | }; 32 | 33 | return next(); 34 | } catch { 35 | throw new AppError('Invalid JTW token', 401); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/routes/password.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { celebrate, Joi, Segments } from 'celebrate'; 3 | 4 | import ForgotPasswordController from '../controllers/ForgotPasswordController'; 5 | import ResetPasswordController from '../controllers/ResetPasswordController'; 6 | 7 | const passwordRouter = Router(); 8 | 9 | const passwordController = new ForgotPasswordController(); 10 | const resetController = new ResetPasswordController(); 11 | 12 | passwordRouter.post( 13 | '/forgot', 14 | celebrate({ 15 | [Segments.BODY]: { email: Joi.string().required() }, 16 | }), 17 | passwordController.create, 18 | ); 19 | passwordRouter.post( 20 | '/reset', 21 | celebrate({ 22 | [Segments.BODY]: { 23 | token: Joi.string().uuid().required(), 24 | password: Joi.string().required(), 25 | password_confirmation: Joi.string().required().valid(Joi.ref('password')), 26 | }, 27 | }), 28 | resetController.create, 29 | ); 30 | 31 | export default passwordRouter; 32 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/routes/profile.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import ensureAuthenticated from '@modules/users/infra/http/middlewares/ensureAuthenticated'; 3 | import { celebrate, Joi, Segments } from 'celebrate'; 4 | 5 | import ProfileController from '../controllers/ProfileController'; 6 | 7 | const profileController = new ProfileController(); 8 | 9 | const profileRouter = Router(); 10 | 11 | profileRouter.use(ensureAuthenticated); 12 | 13 | profileRouter.get('/', profileController.show); 14 | profileRouter.put( 15 | '/', 16 | celebrate({ 17 | [Segments.BODY]: { 18 | name: Joi.string().required(), 19 | email: Joi.string().email().required(), 20 | old_password: Joi.string(), 21 | password: Joi.string(), 22 | confirmed_password: Joi.string().valid(Joi.ref('password')), 23 | }, 24 | }), 25 | profileController.update, 26 | ); 27 | 28 | export default profileRouter; 29 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/routes/sessions.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { celebrate, Joi, Segments } from 'celebrate'; 3 | 4 | import SessionsController from '../controllers/SessionsController'; 5 | 6 | const sessionsRouter = Router(); 7 | 8 | const sessionsController = new SessionsController(); 9 | 10 | sessionsRouter.post( 11 | '/', 12 | celebrate({ 13 | [Segments.BODY]: { 14 | email: Joi.string().email().required(), 15 | password: Joi.string().required(), 16 | }, 17 | }), 18 | sessionsController.create, 19 | ); 20 | 21 | export default sessionsRouter; 22 | -------------------------------------------------------------------------------- /src/modules/users/infra/http/routes/users.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import multer from 'multer'; 3 | import uploadConfig from '@config/upload'; 4 | import ensureAuthenticated from '@modules/users/infra/http/middlewares/ensureAuthenticated'; 5 | import { celebrate, Joi, Segments } from 'celebrate'; 6 | 7 | import UsersController from '../controllers/UsersController'; 8 | import UserAvatarController from '../controllers/UserAvatarController'; 9 | 10 | const usersRouter = Router(); 11 | const upload = multer(uploadConfig.multer); 12 | 13 | const usersController = new UsersController(); 14 | const userAvatarController = new UserAvatarController(); 15 | 16 | usersRouter.post( 17 | '/', 18 | celebrate({ 19 | [Segments.BODY]: { 20 | name: Joi.string().required(), 21 | email: Joi.string().email().required(), 22 | password: Joi.string().required(), 23 | }, 24 | }), 25 | usersController.create, 26 | ); 27 | 28 | usersRouter.patch( 29 | '/avatar', 30 | ensureAuthenticated, 31 | upload.single('avatar'), 32 | userAvatarController.update, 33 | ); 34 | 35 | export default usersRouter; 36 | -------------------------------------------------------------------------------- /src/modules/users/infra/typeorm/entities/User.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | } from 'typeorm'; 8 | import uploadConfig from '@config/upload'; 9 | 10 | import { Expose, Exclude } from 'class-transformer'; 11 | 12 | @Entity('users') 13 | class User { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Column() 18 | name: string; 19 | 20 | @Column() 21 | email: string; 22 | 23 | @Column() 24 | @Exclude() 25 | password: string; 26 | 27 | @Column() 28 | avatar: string; 29 | 30 | @CreateDateColumn() 31 | created_at: Date; 32 | 33 | @UpdateDateColumn() 34 | updated_at: Date; 35 | 36 | @Expose({ name: 'avatar_url' }) 37 | getAvatarUrl(): string | null { 38 | if (!this.avatar) { 39 | return null; 40 | } 41 | switch (uploadConfig.driver) { 42 | case 'disk': 43 | return `${process.env.APP_API_URL}/files/${this.avatar}`; 44 | 45 | case 's3': 46 | return `https://${uploadConfig.config.aws.bucket}.s3.amazonaws.com/${this.avatar}`; 47 | default: 48 | return null; 49 | } 50 | } 51 | } 52 | 53 | export default User; 54 | -------------------------------------------------------------------------------- /src/modules/users/infra/typeorm/entities/UserToken.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | Generated, 8 | } from 'typeorm'; 9 | 10 | @Entity('user_tokens') 11 | class UserToken { 12 | @PrimaryGeneratedColumn('uuid') 13 | id: string; 14 | 15 | @Column() 16 | @Generated('uuid') 17 | token: string; 18 | 19 | @Column() 20 | user_id: string; 21 | 22 | @CreateDateColumn() 23 | created_at: Date; 24 | 25 | @UpdateDateColumn() 26 | updated_at: Date; 27 | } 28 | 29 | export default UserToken; 30 | -------------------------------------------------------------------------------- /src/modules/users/infra/typeorm/repositories/UserTokensRepository.ts: -------------------------------------------------------------------------------- 1 | import { getRepository, Repository } from 'typeorm'; 2 | import IUserTokensRepository from '@modules/users/repositories/IUserTokensRepository'; 3 | import UserToken from '../entities/UserToken'; 4 | 5 | class UserTokensRepository implements IUserTokensRepository { 6 | private ormRepository: Repository; 7 | 8 | constructor() { 9 | this.ormRepository = getRepository(UserToken); 10 | } 11 | 12 | public async findByToken(token: string): Promise { 13 | const user = await this.ormRepository.findOne({ where: { token } }); 14 | return user; 15 | } 16 | 17 | public async generate(user_id: string): Promise { 18 | const userToken = this.ormRepository.create({ 19 | user_id, 20 | }); 21 | 22 | await this.ormRepository.save(userToken); 23 | 24 | return userToken; 25 | } 26 | } 27 | 28 | export default UserTokensRepository; 29 | -------------------------------------------------------------------------------- /src/modules/users/infra/typeorm/repositories/UsersRepository.ts: -------------------------------------------------------------------------------- 1 | import { getRepository, Repository, Not } from 'typeorm'; 2 | import ICreateUserDTO from '@modules/users/dtos/ICreateUserDTO'; 3 | import IUsersRepository from '@modules/users/repositories/IUsersRepository'; 4 | import IFindAllProvidersDTO from '@modules/users/dtos/IFindAllProvidersDTO'; 5 | import User from '../entities/User'; 6 | 7 | class UsersRepository implements IUsersRepository { 8 | private ormRepository: Repository; 9 | 10 | constructor() { 11 | this.ormRepository = getRepository(User); 12 | } 13 | 14 | public async findAllProviders({ 15 | except_user_id, 16 | }: IFindAllProvidersDTO): Promise { 17 | let users: User[]; 18 | 19 | if (except_user_id) { 20 | users = await this.ormRepository.find({ 21 | where: { 22 | id: Not(except_user_id), 23 | }, 24 | }); 25 | } else { 26 | users = await this.ormRepository.find(); 27 | } 28 | 29 | return users; 30 | } 31 | 32 | public async findById(id: string): Promise { 33 | const user = await this.ormRepository.findOne(id); 34 | return user; 35 | } 36 | 37 | public async findByEmail(email: string): Promise { 38 | const user = await this.ormRepository.findOne({ where: { email } }); 39 | return user; 40 | } 41 | 42 | public async create(userData: ICreateUserDTO): Promise { 43 | const user = this.ormRepository.create(userData); 44 | 45 | await this.ormRepository.save(user); 46 | 47 | return user; 48 | } 49 | 50 | public async save(user: User): Promise { 51 | return this.ormRepository.save(user); 52 | } 53 | } 54 | 55 | export default UsersRepository; 56 | -------------------------------------------------------------------------------- /src/modules/users/providers/HashProvider/fakes/FakeHashProvider.ts: -------------------------------------------------------------------------------- 1 | import IHashProvider from '../models/IHashProvider'; 2 | 3 | class FakeHashProvider implements IHashProvider { 4 | public async generateHash(payload: string): Promise { 5 | return payload; 6 | } 7 | 8 | public async compareHash(payload: string, hashed: string): Promise { 9 | return payload === hashed; 10 | } 11 | } 12 | 13 | export default FakeHashProvider; 14 | -------------------------------------------------------------------------------- /src/modules/users/providers/HashProvider/implementations/BCryptHashProvider.ts: -------------------------------------------------------------------------------- 1 | import { hash, compare } from 'bcryptjs'; 2 | import IHashProvider from '../models/IHashProvider'; 3 | 4 | class BCryptHashProvider implements IHashProvider { 5 | public async generateHash(payload: string): Promise { 6 | return hash(payload, 8); 7 | } 8 | 9 | public async compareHash(payload: string, hashed: string): Promise { 10 | return compare(payload, hashed); 11 | } 12 | } 13 | 14 | export default BCryptHashProvider; 15 | -------------------------------------------------------------------------------- /src/modules/users/providers/HashProvider/models/IHashProvider.ts: -------------------------------------------------------------------------------- 1 | export default interface IHashProvider { 2 | generateHash(payload: string): Promise; 3 | compareHash(payload: string, hashed: string): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/users/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from 'tsyringe'; 2 | 3 | import IHashProvider from './HashProvider/models/IHashProvider'; 4 | import BCryptHashProvider from './HashProvider/implementations/BCryptHashProvider'; 5 | 6 | container.registerSingleton('HashProvider', BCryptHashProvider); 7 | -------------------------------------------------------------------------------- /src/modules/users/repositories/IUserTokensRepository.ts: -------------------------------------------------------------------------------- 1 | import UserToken from '../infra/typeorm/entities/UserToken'; 2 | 3 | export default interface IUserTokensRepository { 4 | generate(user_id: string): Promise; 5 | findByToken(token: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/users/repositories/IUsersRepository.ts: -------------------------------------------------------------------------------- 1 | import User from '../infra/typeorm/entities/User'; 2 | 3 | import ICreateUserDTO from '../dtos/ICreateUserDTO'; 4 | import IFindAllProvidersDTO from '../dtos/IFindAllProvidersDTO'; 5 | 6 | export default interface IUsersRepository { 7 | findById(id: string): Promise; 8 | findByEmail(email: string): Promise; 9 | create(data: ICreateUserDTO): Promise; 10 | save(user: User): Promise; 11 | findAllProviders(data: IFindAllProvidersDTO): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/users/repositories/fakes/FakeUserTokensRepository.ts: -------------------------------------------------------------------------------- 1 | import IUserTokensRepository from '@modules/users/repositories/IUserTokensRepository'; 2 | import { uuid } from 'uuidv4'; 3 | import UserToken from '../../infra/typeorm/entities/UserToken'; 4 | 5 | class FakeUserTokensRepository implements IUserTokensRepository { 6 | private userTokens: UserToken[] = []; 7 | 8 | public async generate(user_id: string): Promise { 9 | const userToken = new UserToken(); 10 | 11 | Object.assign(userToken, { 12 | id: uuid(), 13 | token: uuid(), 14 | user_id, 15 | created_at: new Date(), 16 | update_at: new Date(), 17 | }); 18 | 19 | this.userTokens.push(userToken); 20 | 21 | return userToken; 22 | } 23 | 24 | public async findByToken(token: string): Promise { 25 | const userToken = this.userTokens.find(user => user.token === token); 26 | return userToken; 27 | } 28 | } 29 | 30 | export default FakeUserTokensRepository; 31 | -------------------------------------------------------------------------------- /src/modules/users/repositories/fakes/FakeUsersRepository.ts: -------------------------------------------------------------------------------- 1 | import ICreateUserDTO from '@modules/users/dtos/ICreateUserDTO'; 2 | import IUsersRepository from '@modules/users/repositories/IUsersRepository'; 3 | import { uuid } from 'uuidv4'; 4 | import IFindAllProvidersDTO from '@modules/users/dtos/IFindAllProvidersDTO'; 5 | import User from '../../infra/typeorm/entities/User'; 6 | 7 | class FakeUsersRepository implements IUsersRepository { 8 | private users: User[] = []; 9 | 10 | public async findAllProviders({ 11 | except_user_id, 12 | }: IFindAllProvidersDTO): Promise { 13 | let { users } = this; 14 | 15 | if (except_user_id) { 16 | users = this.users.filter(user => user.id !== except_user_id); 17 | } 18 | 19 | return users; 20 | } 21 | 22 | public async findById(id: string): Promise { 23 | return this.users.find(user => user.id === id); 24 | } 25 | 26 | public async findByEmail(email: string): Promise { 27 | return this.users.find(user => user.email === email); 28 | } 29 | 30 | public async create(userData: ICreateUserDTO): Promise { 31 | const user = new User(); 32 | 33 | Object.assign(user, { id: uuid() }, userData); 34 | 35 | this.users.push(user); 36 | 37 | return user; 38 | } 39 | 40 | public async save(user: User): Promise { 41 | const findIndex = this.users.findIndex(findUser => findUser.id === user.id); 42 | 43 | this.users[findIndex] = user; 44 | 45 | return user; 46 | } 47 | } 48 | 49 | export default FakeUsersRepository; 50 | -------------------------------------------------------------------------------- /src/modules/users/services/AuthenticateUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeCacheProvider from '@shared/container/providers/CacheProvider/implementations/fakes/FakeCacheProvider'; 3 | import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository'; 4 | import FakeHashProvider from '../providers/HashProvider/fakes/FakeHashProvider'; 5 | import AuthenticateUserService from './AuthenticateUserService'; 6 | import CreateUserService from './CreateUserService'; 7 | 8 | describe('AuthenticateUser', () => { 9 | let fakeUsersRepository: FakeUsersRepository; 10 | let fakeHashProvider: FakeHashProvider; 11 | let authenticateUser: AuthenticateUserService; 12 | let fakeCacheProvider: FakeCacheProvider; 13 | 14 | beforeEach(() => { 15 | fakeUsersRepository = new FakeUsersRepository(); 16 | fakeHashProvider = new FakeHashProvider(); 17 | fakeCacheProvider = new FakeCacheProvider(); 18 | 19 | authenticateUser = new AuthenticateUserService( 20 | fakeUsersRepository, 21 | fakeHashProvider, 22 | ); 23 | }); 24 | 25 | it('should be able to authenticate user', async () => { 26 | const createUser = new CreateUserService( 27 | fakeUsersRepository, 28 | fakeHashProvider, 29 | fakeCacheProvider, 30 | ); 31 | 32 | const user = await createUser.execute({ 33 | name: 'Thiago Marinho', 34 | email: 'tgmarinho@gmail.com', 35 | password: '123456', 36 | }); 37 | 38 | const response = await authenticateUser.execute({ 39 | email: 'tgmarinho@gmail.com', 40 | password: '123456', 41 | }); 42 | 43 | expect(response).toHaveProperty('token'); 44 | expect(response.user).toEqual(user); 45 | }); 46 | 47 | it('should not be able to authenticate with non existing user', async () => { 48 | await expect( 49 | authenticateUser.execute({ 50 | email: 'tgmarinho@gmail.com', 51 | password: '123456', 52 | }), 53 | ).rejects.toBeInstanceOf(AppError); 54 | }); 55 | 56 | it('should not be able to authenticate with wrong password', async () => { 57 | const createUser = new CreateUserService( 58 | fakeUsersRepository, 59 | fakeHashProvider, 60 | fakeCacheProvider, 61 | ); 62 | 63 | await createUser.execute({ 64 | name: 'Thiago Marinho', 65 | email: 'tgmarinho@gmail.com', 66 | password: '123456', 67 | }); 68 | 69 | await expect( 70 | authenticateUser.execute({ 71 | email: 'tgmarinho@gmail.com', 72 | password: 'xxxxx', 73 | }), 74 | ).rejects.toBeInstanceOf(AppError); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/modules/users/services/AuthenticateUserService.ts: -------------------------------------------------------------------------------- 1 | import { sign } from 'jsonwebtoken'; 2 | import User from '@modules/users/infra/typeorm/entities/User'; 3 | import authConfig from '@config/auth'; 4 | import AppError from '@shared/errors/AppError'; 5 | import { injectable, inject } from 'tsyringe'; 6 | import IHashProvider from '@modules/users/providers/HashProvider/models/IHashProvider'; 7 | import IUsersRepository from '../repositories/IUsersRepository'; 8 | 9 | interface IRequest { 10 | email: string; 11 | password: string; 12 | } 13 | 14 | interface IResponse { 15 | user: User; 16 | token: string; 17 | } 18 | 19 | @injectable() 20 | class AuthenticateUserService { 21 | constructor( 22 | @inject('UsersRepository') 23 | private usersRepository: IUsersRepository, 24 | 25 | @inject('HashProvider') 26 | private hashProvider: IHashProvider, 27 | ) {} 28 | 29 | public async execute({ email, password }: IRequest): Promise { 30 | const user = await this.usersRepository.findByEmail(email); 31 | 32 | if (!user) { 33 | throw new AppError('Incorrect email/password combination.', 401); 34 | } 35 | 36 | const passwordMatched = await this.hashProvider.compareHash( 37 | password, 38 | user.password, 39 | ); 40 | 41 | if (!passwordMatched) { 42 | throw new AppError('Incorrect email/password combination.', 401); 43 | } 44 | 45 | const { secret, expiresIn } = authConfig.jwt; 46 | 47 | const token = sign({}, secret, { 48 | subject: user.id, 49 | expiresIn, 50 | }); 51 | 52 | delete user.password; 53 | 54 | return { user, token }; 55 | } 56 | } 57 | 58 | export default AuthenticateUserService; 59 | -------------------------------------------------------------------------------- /src/modules/users/services/CreateUserService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeCacheProvider from '@shared/container/providers/CacheProvider/implementations/fakes/FakeCacheProvider'; 3 | import FakeHashProvider from '../providers/HashProvider/fakes/FakeHashProvider'; 4 | import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository'; 5 | import CreateUserService from './CreateUserService'; 6 | 7 | describe('CreateUser', () => { 8 | let fakeUsersRepository: FakeUsersRepository; 9 | let fakeHashProvider: FakeHashProvider; 10 | let fakeCacheProvider: FakeCacheProvider; 11 | let createUserService: CreateUserService; 12 | 13 | beforeEach(() => { 14 | fakeUsersRepository = new FakeUsersRepository(); 15 | fakeHashProvider = new FakeHashProvider(); 16 | fakeCacheProvider = new FakeCacheProvider(); 17 | createUserService = new CreateUserService( 18 | fakeUsersRepository, 19 | fakeHashProvider, 20 | fakeCacheProvider, 21 | ); 22 | }); 23 | 24 | it('should be able to create a new user', async () => { 25 | const user = await createUserService.execute({ 26 | name: 'Thiago Marinho', 27 | email: 'tgmarinho@gmail.com', 28 | password: '123456', 29 | }); 30 | 31 | expect(user).toHaveProperty('id'); 32 | expect(user).toHaveProperty('password'); 33 | expect(user.email).toBe('tgmarinho@gmail.com'); 34 | }); 35 | 36 | it('should not be able to create new user with the same email from another', async () => { 37 | const inputUser = { 38 | name: 'Thiago Marinho', 39 | email: 'tgmarinho@gmail.com', 40 | password: '123456', 41 | }; 42 | 43 | await createUserService.execute(inputUser); 44 | 45 | await expect(createUserService.execute(inputUser)).rejects.toBeInstanceOf( 46 | AppError, 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/modules/users/services/CreateUserService.ts: -------------------------------------------------------------------------------- 1 | import User from '@modules/users/infra/typeorm/entities/User'; 2 | import AppError from '@shared/errors/AppError'; 3 | import { injectable, inject } from 'tsyringe'; 4 | import IHashProvider from '@modules/users/providers/HashProvider/models/IHashProvider'; 5 | import ICacheProvider from '@shared/container/providers/CacheProvider/models/ICacheProvider'; 6 | import IUsersRepository from '../repositories/IUsersRepository'; 7 | 8 | interface IRequest { 9 | name: string; 10 | email: string; 11 | password: string; 12 | } 13 | 14 | @injectable() 15 | class CreateUserService { 16 | constructor( 17 | @inject('UsersRepository') 18 | private usersRepository: IUsersRepository, 19 | 20 | @inject('HashProvider') 21 | private hashProvider: IHashProvider, 22 | 23 | @inject('CacheProvider') 24 | private cacheProvider: ICacheProvider, 25 | ) {} 26 | 27 | public async execute({ name, email, password }: IRequest): Promise { 28 | const checkUserExists = await this.usersRepository.findByEmail(email); 29 | 30 | if (checkUserExists) { 31 | throw new AppError('Email address already used.'); 32 | } 33 | 34 | const hashedPassword = await this.hashProvider.generateHash(password); 35 | 36 | const user = await this.usersRepository.create({ 37 | name, 38 | email, 39 | password: hashedPassword, 40 | }); 41 | 42 | await this.cacheProvider.invalidatePrefix('providers-list'); 43 | 44 | return user; 45 | } 46 | } 47 | 48 | export default CreateUserService; 49 | -------------------------------------------------------------------------------- /src/modules/users/services/ResetPasswordService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeHashProvider from '../providers/HashProvider/fakes/FakeHashProvider'; 3 | import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository'; 4 | import ResetPasswordService from './ResetPasswordService'; 5 | import FakeUserTokensRepository from '../repositories/fakes/FakeUserTokensRepository'; 6 | 7 | let fakeUsersRepository: FakeUsersRepository; 8 | let fakeUserTokensRepository: FakeUserTokensRepository; 9 | let resetPassword: ResetPasswordService; 10 | let fakeHashProvider: FakeHashProvider; 11 | 12 | describe('ResetPasswordService', () => { 13 | beforeEach(() => { 14 | fakeUsersRepository = new FakeUsersRepository(); 15 | fakeUserTokensRepository = new FakeUserTokensRepository(); 16 | fakeHashProvider = new FakeHashProvider(); 17 | 18 | resetPassword = new ResetPasswordService( 19 | fakeUsersRepository, 20 | fakeUserTokensRepository, 21 | fakeHashProvider, 22 | ); 23 | }); 24 | 25 | it('should be able to reset the password', async () => { 26 | const user = await fakeUsersRepository.create({ 27 | name: 'Thiago Marinho', 28 | email: 'tgmarinho@gmail.com', 29 | password: '123456', 30 | }); 31 | 32 | const userToken = await fakeUserTokensRepository.generate(user.id); 33 | 34 | const generateHash = jest.spyOn(fakeHashProvider, 'generateHash'); 35 | 36 | await resetPassword.execute({ 37 | password: '123123', 38 | token: userToken.token, 39 | }); 40 | 41 | const userUpdated = await fakeUsersRepository.findById(user.id); 42 | 43 | expect(generateHash).toHaveBeenCalledWith('123123'); 44 | expect(userUpdated?.password).toBe('123123'); 45 | }); 46 | 47 | it('should not be able to reset the password with non-existing token', async () => { 48 | await expect( 49 | resetPassword.execute({ 50 | token: 'non-existing-token', 51 | password: '123457', 52 | }), 53 | ).rejects.toBeInstanceOf(AppError); 54 | }); 55 | 56 | it('should not be able to reset the password with non-existing user', async () => { 57 | const { token } = await fakeUserTokensRepository.generate( 58 | 'non-existing-user', 59 | ); 60 | 61 | await expect( 62 | resetPassword.execute({ 63 | token, 64 | password: '123457', 65 | }), 66 | ).rejects.toBeInstanceOf(AppError); 67 | }); 68 | 69 | it('should not be able to reset the password if passed more than two hours', async () => { 70 | const user = await fakeUsersRepository.create({ 71 | name: 'Thiago Marinho', 72 | email: 'tgmarinho@gmail.com', 73 | password: '123456', 74 | }); 75 | 76 | const { token } = await fakeUserTokensRepository.generate(user.id); 77 | 78 | jest.spyOn(Date, 'now').mockImplementationOnce(() => { 79 | const customDate = new Date(); 80 | return customDate.setHours(customDate.getHours() + 3); 81 | }); 82 | 83 | await expect( 84 | resetPassword.execute({ 85 | password: '123123', 86 | token, 87 | }), 88 | ).rejects.toBeInstanceOf(AppError); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/modules/users/services/ResetPasswordService.ts: -------------------------------------------------------------------------------- 1 | // import User from '@modules/users/infra/typeorm/entities/User'; 2 | import AppError from '@shared/errors/AppError'; 3 | import { injectable, inject } from 'tsyringe'; 4 | import { differenceInHours } from 'date-fns'; 5 | import IUsersRepository from '../repositories/IUsersRepository'; 6 | import IUserTokensRepository from '../repositories/IUserTokensRepository'; 7 | import IHashProvider from '../providers/HashProvider/models/IHashProvider'; 8 | 9 | interface IRequest { 10 | password: string; 11 | token: string; 12 | } 13 | 14 | @injectable() 15 | class ResetPasswordService { 16 | constructor( 17 | @inject('UsersRepository') 18 | private usersRepository: IUsersRepository, 19 | 20 | @inject('UserTokensRepository') 21 | private userTokensRepository: IUserTokensRepository, 22 | 23 | @inject('HashProvider') 24 | private hashProvider: IHashProvider, 25 | ) {} 26 | 27 | public async execute({ token, password }: IRequest): Promise { 28 | const userToken = await this.userTokensRepository.findByToken(token); 29 | if (!userToken) { 30 | throw new AppError('User token does not exists'); 31 | } 32 | const user = await this.usersRepository.findById(userToken.user_id); 33 | 34 | if (!user) { 35 | throw new AppError('User token does not exists'); 36 | } 37 | 38 | const tokenCreatedAt = userToken.created_at; 39 | 40 | if (differenceInHours(Date.now(), tokenCreatedAt) > 2) { 41 | throw new AppError('Token expired.'); 42 | } 43 | 44 | user.password = await this.hashProvider.generateHash(password); 45 | 46 | await this.usersRepository.save(user); 47 | } 48 | } 49 | 50 | export default ResetPasswordService; 51 | -------------------------------------------------------------------------------- /src/modules/users/services/SendForgotPasswordEmailService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeMailProvider from '@shared/container/providers/MailProvider/fakes/FakeMailProvider'; 3 | import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository'; 4 | import SendForgotPasswordEmailService from './SendForgotPasswordEmailService'; 5 | import FakeUserTokensRepository from '../repositories/fakes/FakeUserTokensRepository'; 6 | 7 | let fakeUsersRepository: FakeUsersRepository; 8 | let fakeMailProvider: FakeMailProvider; 9 | let fakeUserTokensRepository: FakeUserTokensRepository; 10 | let sendForgotPasswordEmail: SendForgotPasswordEmailService; 11 | 12 | describe('SendForgotPassowordService', () => { 13 | beforeEach(() => { 14 | fakeUsersRepository = new FakeUsersRepository(); 15 | fakeUserTokensRepository = new FakeUserTokensRepository(); 16 | fakeMailProvider = new FakeMailProvider(); 17 | 18 | sendForgotPasswordEmail = new SendForgotPasswordEmailService( 19 | fakeUsersRepository, 20 | fakeMailProvider, 21 | fakeUserTokensRepository, 22 | ); 23 | }); 24 | 25 | it('should be able to recover the password using the email', async () => { 26 | const sendMail = jest.spyOn(fakeMailProvider, 'sendMail'); 27 | 28 | await fakeUsersRepository.create({ 29 | name: 'Thiago Marinho', 30 | email: 'tgmarinho@gmail.com', 31 | password: '123456', 32 | }); 33 | 34 | await sendForgotPasswordEmail.execute({ 35 | email: 'tgmarinho@gmail.com', 36 | }); 37 | 38 | expect(sendMail).toHaveBeenCalled(); 39 | }); 40 | 41 | it('should not be able to recover a non-existing user password', async () => { 42 | await expect( 43 | sendForgotPasswordEmail.execute({ 44 | email: 'tgmarinho@gmail.com', 45 | }), 46 | ).rejects.toBeInstanceOf(AppError); 47 | }); 48 | 49 | it('should generate a forgot password token', async () => { 50 | const generateToken = jest.spyOn(fakeUserTokensRepository, 'generate'); 51 | 52 | const user = await fakeUsersRepository.create({ 53 | name: 'Thiago Marinho', 54 | email: 'tgmarinho@gmail.com', 55 | password: '123456', 56 | }); 57 | 58 | await sendForgotPasswordEmail.execute({ 59 | email: user.email, 60 | }); 61 | 62 | expect(generateToken).toHaveBeenCalledWith(user.id); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/modules/users/services/SendForgotPasswordEmailService.ts: -------------------------------------------------------------------------------- 1 | // import User from '@modules/users/infra/typeorm/entities/User'; 2 | import AppError from '@shared/errors/AppError'; 3 | import { injectable, inject } from 'tsyringe'; 4 | import IMailProvider from '@shared/container/providers/MailProvider/models/IMailProvider'; 5 | import path from 'path'; 6 | import IUsersRepository from '../repositories/IUsersRepository'; 7 | import IUserTokensRepository from '../repositories/IUserTokensRepository'; 8 | 9 | interface IRequest { 10 | email: string; 11 | } 12 | 13 | @injectable() 14 | class SendForgotPasswordEmailService { 15 | constructor( 16 | @inject('UsersRepository') 17 | private usersRepository: IUsersRepository, 18 | 19 | @inject('MailProvider') 20 | private mailProvider: IMailProvider, 21 | 22 | @inject('UserTokensRepository') 23 | private userTokensRepository: IUserTokensRepository, 24 | ) {} 25 | 26 | public async execute({ email }: IRequest): Promise { 27 | const user = await this.usersRepository.findByEmail(email); 28 | 29 | if (!user) { 30 | throw new AppError('User does not exists.'); 31 | } 32 | 33 | const { token } = await this.userTokensRepository.generate(user.id); 34 | 35 | const forgotPasswordTemplate = path.resolve( 36 | __dirname, 37 | '..', 38 | 'views', 39 | 'forgot_password.hbs', 40 | ); 41 | 42 | await this.mailProvider.sendMail({ 43 | to: { name: user.name, email: user.email }, 44 | subject: '[GoBarber] Recuperação de senha', 45 | templateData: { 46 | file: forgotPasswordTemplate, 47 | variables: { 48 | name: user.name, 49 | link: `${process.env.APP_WEB_URL}/reset_password?token=${token}`, 50 | }, 51 | }, 52 | }); 53 | } 54 | } 55 | 56 | export default SendForgotPasswordEmailService; 57 | -------------------------------------------------------------------------------- /src/modules/users/services/ShowProfileService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository'; 3 | import ShowProfileService from './ShowProfileService'; 4 | 5 | describe('ShowUserAvatar', () => { 6 | let fakeUsersRepository: FakeUsersRepository; 7 | let showProfile: ShowProfileService; 8 | 9 | beforeEach(() => { 10 | fakeUsersRepository = new FakeUsersRepository(); 11 | 12 | showProfile = new ShowProfileService(fakeUsersRepository); 13 | }); 14 | 15 | it('should be able to show the profile', async () => { 16 | const user = await fakeUsersRepository.create({ 17 | name: 'Thiago Marinho', 18 | email: 'tgmarinho@gmail.com', 19 | password: '123456', 20 | }); 21 | 22 | const profile = await showProfile.execute({ 23 | user_id: user.id, 24 | }); 25 | 26 | expect(profile.name).toBe('Thiago Marinho'); 27 | expect(profile.email).toBe('tgmarinho@gmail.com'); 28 | }); 29 | 30 | it('should not be able to show the profile from non-existing user', async () => { 31 | await expect( 32 | showProfile.execute({ 33 | user_id: 'non-existing-user_id', 34 | }), 35 | ).rejects.toBeInstanceOf(AppError); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/modules/users/services/ShowProfileService.ts: -------------------------------------------------------------------------------- 1 | import User from '@modules/users/infra/typeorm/entities/User'; 2 | import AppError from '@shared/errors/AppError'; 3 | import { injectable, inject } from 'tsyringe'; 4 | import IUsersRepository from '../repositories/IUsersRepository'; 5 | 6 | interface IRequest { 7 | user_id: string; 8 | } 9 | 10 | @injectable() 11 | class ShowProfileService { 12 | constructor( 13 | @inject('UsersRepository') 14 | private usersRepository: IUsersRepository, 15 | ) {} 16 | 17 | public async execute({ user_id }: IRequest): Promise { 18 | const user = await this.usersRepository.findById(user_id); 19 | if (!user) { 20 | throw new AppError('User not found'); 21 | } 22 | 23 | return user; 24 | } 25 | } 26 | 27 | export default ShowProfileService; 28 | -------------------------------------------------------------------------------- /src/modules/users/services/UpdateProfileService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeHashProvider from '@modules/users/providers/HashProvider/fakes/FakeHashProvider'; 3 | import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository'; 4 | import UpdateProfileService from './UpdateProfileService'; 5 | 6 | describe('UpdateUserAvatar', () => { 7 | let fakeUsersRepository: FakeUsersRepository; 8 | let fakeHashProvider: FakeHashProvider; 9 | let updateProfile: UpdateProfileService; 10 | 11 | beforeEach(() => { 12 | fakeUsersRepository = new FakeUsersRepository(); 13 | fakeHashProvider = new FakeHashProvider(); 14 | 15 | updateProfile = new UpdateProfileService( 16 | fakeUsersRepository, 17 | fakeHashProvider, 18 | ); 19 | }); 20 | 21 | it('should be able to update profile of the user', async () => { 22 | const user = await fakeUsersRepository.create({ 23 | name: 'Thiago Marinho', 24 | email: 'tgmarinho@gmail.com', 25 | password: '123456', 26 | }); 27 | 28 | const updatedUser = await updateProfile.execute({ 29 | user_id: user.id, 30 | name: 'Tiago Mariano', 31 | email: 'tgmariano@gmail.com', 32 | }); 33 | 34 | expect(updatedUser.name).toBe('Tiago Mariano'); 35 | expect(updatedUser.email).toBe('tgmariano@gmail.com'); 36 | }); 37 | 38 | it('should not be able to change to another user email', async () => { 39 | await fakeUsersRepository.create({ 40 | name: 'Thiago Marinho', 41 | email: 'tgmarinho@gmail.com', 42 | password: '123456', 43 | }); 44 | 45 | const user = await fakeUsersRepository.create({ 46 | name: 'Test', 47 | email: 'test@sample.com', 48 | password: '123456', 49 | }); 50 | 51 | await expect( 52 | updateProfile.execute({ 53 | user_id: user.id, 54 | name: 'Thiago Marinho', 55 | email: 'tgmarinho@gmail.com', 56 | }), 57 | ).rejects.toBeInstanceOf(AppError); 58 | }); 59 | 60 | it('should be able to update the password', async () => { 61 | const user = await fakeUsersRepository.create({ 62 | name: 'Thiago Marinho', 63 | email: 'tgmarinho@gmail.com', 64 | password: '123456', 65 | }); 66 | 67 | const updatedUser = await updateProfile.execute({ 68 | user_id: user.id, 69 | name: 'Tiago Mariano', 70 | email: 'tgmariano@gmail.com', 71 | old_password: '123456', 72 | password: '123123', 73 | }); 74 | 75 | expect(updatedUser.password).toBe('123123'); 76 | }); 77 | 78 | it('should not be able to update the password without inform the old password', async () => { 79 | const user = await fakeUsersRepository.create({ 80 | name: 'Thiago Marinho', 81 | email: 'tgmarinho@gmail.com', 82 | password: '123456', 83 | }); 84 | 85 | await expect( 86 | updateProfile.execute({ 87 | user_id: user.id, 88 | name: 'Tiago Mariano', 89 | email: 'tgmariano@gmail.com', 90 | // old passwd is not informed 91 | password: '123123', 92 | }), 93 | ).rejects.toBeInstanceOf(AppError); 94 | }); 95 | 96 | it('should not be able to update the password with wrong old password', async () => { 97 | const user = await fakeUsersRepository.create({ 98 | name: 'Thiago Marinho', 99 | email: 'tgmarinho@gmail.com', 100 | password: '123456', 101 | }); 102 | 103 | await expect( 104 | updateProfile.execute({ 105 | user_id: user.id, 106 | name: 'Tiago Mariano', 107 | email: 'tgmariano@gmail.com', 108 | old_password: 'xx-wrong-password-xx', 109 | password: '123123', 110 | }), 111 | ).rejects.toBeInstanceOf(AppError); 112 | }); 113 | 114 | it('should not be able to update profile from non-existing user', async () => { 115 | await expect( 116 | updateProfile.execute({ 117 | user_id: 'non_existing_user_id_', 118 | name: 'Tiago Mariano', 119 | email: 'tgmariano@gmail.com', 120 | old_password: 'xx-wrong-password-xx', 121 | password: '123123', 122 | }), 123 | ).rejects.toBeInstanceOf(AppError); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/modules/users/services/UpdateProfileService.ts: -------------------------------------------------------------------------------- 1 | import User from '@modules/users/infra/typeorm/entities/User'; 2 | import AppError from '@shared/errors/AppError'; 3 | import { injectable, inject } from 'tsyringe'; 4 | import IHashProvider from '@modules/users/providers/HashProvider/models/IHashProvider'; 5 | import IUsersRepository from '../repositories/IUsersRepository'; 6 | 7 | interface IRequest { 8 | user_id: string; 9 | name: string; 10 | email: string; 11 | old_password?: string; 12 | password?: string; 13 | } 14 | 15 | @injectable() 16 | class UpdateProfileService { 17 | constructor( 18 | @inject('UsersRepository') 19 | private usersRepository: IUsersRepository, 20 | 21 | @inject('HashProvider') 22 | private hashProvider: IHashProvider, 23 | ) {} 24 | 25 | public async execute({ 26 | user_id, 27 | name, 28 | email, 29 | old_password, 30 | password, 31 | }: IRequest): Promise { 32 | const user = await this.usersRepository.findById(user_id); 33 | if (!user) { 34 | throw new AppError('User not found'); 35 | } 36 | 37 | const userWithUpdatedEmail = await this.usersRepository.findByEmail(email); 38 | 39 | if (userWithUpdatedEmail && userWithUpdatedEmail.id !== user_id) { 40 | throw new AppError('Already have a user with this email'); 41 | } 42 | 43 | user.name = name; 44 | user.email = email; 45 | 46 | if (password && !old_password) { 47 | throw new AppError( 48 | 'You need to inform the old password to set a new password', 49 | ); 50 | } 51 | 52 | if (password && old_password) { 53 | const checkOldPassword = await this.hashProvider.compareHash( 54 | old_password, 55 | user.password, 56 | ); 57 | 58 | if (!checkOldPassword) { 59 | throw new AppError('Old password does not macth'); 60 | } 61 | 62 | user.password = await this.hashProvider.generateHash(password); 63 | } 64 | 65 | await this.usersRepository.save(user); 66 | 67 | return user; 68 | } 69 | } 70 | 71 | export default UpdateProfileService; 72 | -------------------------------------------------------------------------------- /src/modules/users/services/UpdateUserAvatarService.spec.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@shared/errors/AppError'; 2 | import FakeStorageProvider from '@shared/container/providers/StorageProvider/fakes/FakeStorageProvider'; 3 | import FakeUsersRepository from '../repositories/fakes/FakeUsersRepository'; 4 | import UpdateUserAvatarService from './UpdateUserAvatarService'; 5 | 6 | describe('UpdateUserAvatar', () => { 7 | let fakeUsersRepository: FakeUsersRepository; 8 | let fakeStorageProvider: FakeStorageProvider; 9 | let updateUserAvatar: UpdateUserAvatarService; 10 | 11 | beforeEach(() => { 12 | fakeUsersRepository = new FakeUsersRepository(); 13 | fakeStorageProvider = new FakeStorageProvider(); 14 | 15 | updateUserAvatar = new UpdateUserAvatarService( 16 | fakeUsersRepository, 17 | fakeStorageProvider, 18 | ); 19 | }); 20 | 21 | it('should be able to update avatar of user', async () => { 22 | const user = await fakeUsersRepository.create({ 23 | name: 'Thiago Marinho', 24 | email: 'tgmarinho@gmail.com', 25 | password: '123456', 26 | }); 27 | 28 | await updateUserAvatar.execute({ 29 | user_id: user.id, 30 | avatarFilename: 'avatar.jpg', 31 | }); 32 | 33 | expect(user.avatar).toBe('avatar.jpg'); 34 | }); 35 | 36 | it('should not be able to update avatar from non existing user', async () => { 37 | expect( 38 | updateUserAvatar.execute({ 39 | user_id: 'non-existing-user', 40 | avatarFilename: 'avatar.jpg', 41 | }), 42 | ).rejects.toBeInstanceOf(AppError); 43 | }); 44 | 45 | it('should delete old avatar when updating new one', async () => { 46 | const deleteFile = jest.spyOn(fakeStorageProvider, 'deleteFile'); 47 | 48 | const user = await fakeUsersRepository.create({ 49 | name: 'Thiago Marinho', 50 | email: 'tgmarinho@gmail.com', 51 | password: '123456', 52 | }); 53 | 54 | await updateUserAvatar.execute({ 55 | user_id: user.id, 56 | avatarFilename: 'avatar.jpg', 57 | }); 58 | 59 | await updateUserAvatar.execute({ 60 | user_id: user.id, 61 | avatarFilename: 'avatar2.jpg', 62 | }); 63 | 64 | expect(deleteFile).toHaveBeenCalledWith('avatar.jpg'); 65 | expect(user.avatar).toBe('avatar2.jpg'); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/modules/users/services/UpdateUserAvatarService.ts: -------------------------------------------------------------------------------- 1 | import User from '@modules/users/infra/typeorm/entities/User'; 2 | import AppError from '@shared/errors/AppError'; 3 | import { injectable, inject } from 'tsyringe'; 4 | import IStorageProvider from '@shared/container/providers/StorageProvider/models/IStorageProvider'; 5 | import IUsersRepository from '../repositories/IUsersRepository'; 6 | 7 | interface IRequest { 8 | user_id: string; 9 | avatarFilename: string; 10 | } 11 | 12 | @injectable() 13 | class UpdateUserAvaterService { 14 | constructor( 15 | @inject('UsersRepository') 16 | private usersRepository: IUsersRepository, 17 | 18 | @inject('StorageProvider') 19 | private storageProvider: IStorageProvider, 20 | ) {} 21 | 22 | public async execute({ user_id, avatarFilename }: IRequest): Promise { 23 | const user = await this.usersRepository.findById(user_id); 24 | 25 | if (!user) { 26 | throw new AppError('Only authenticated users can change avatar.'); 27 | } 28 | 29 | // deletar avatar anterior 30 | if (user.avatar) { 31 | await this.storageProvider.deleteFile(user.avatar); 32 | } 33 | 34 | const filename = await this.storageProvider.saveFile(avatarFilename); 35 | 36 | user.avatar = filename; 37 | 38 | await this.usersRepository.save(user); 39 | 40 | return user; 41 | } 42 | } 43 | 44 | export default UpdateUserAvaterService; 45 | -------------------------------------------------------------------------------- /src/modules/users/views/forgot_password.hbs: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |

Olá, {{name}}

12 |

Parece que uma troca de senha para sua conta foi solicitada.

13 |

Se foi você, então clique no link abaixo para escolher uma nova senha.

14 |

15 | Resetar minha senha 16 |

17 |

Se não foi você, então desconsidere esse email!

18 |

19 | Obrigado!
20 | Equipe GoBarber 21 |

22 | 23 |
24 | -------------------------------------------------------------------------------- /src/shared/container/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from 'tsyringe'; 2 | 3 | import '@modules/users/providers'; 4 | import './providers'; 5 | 6 | import IAppointmentsRepository from '@modules/appointments/repositories/IAppointmentsRepository'; 7 | import Appointmentsrepository from '@modules/appointments/infra/typeorm/repositories/AppointmentsRepository'; 8 | 9 | import IUsersRepository from '@modules/users/repositories/IUsersRepository'; 10 | import UsersRepository from '@modules/users/infra/typeorm/repositories/UsersRepository'; 11 | 12 | import IUserTokensRepository from '@modules/users/repositories/IUserTokensRepository'; 13 | import UserTokensRepository from '@modules/users/infra/typeorm/repositories/UserTokensRepository'; 14 | 15 | import INotificationsRepository from '@modules/notifications/repositories/INotificationsRepository'; 16 | import NotificationsRepository from '@modules/notifications/infra/typeorm/repositories/NotificationsRepository'; 17 | 18 | container.registerSingleton( 19 | 'AppointmentsRepository', 20 | Appointmentsrepository, 21 | ); 22 | 23 | container.registerSingleton( 24 | 'UsersRepository', 25 | UsersRepository, 26 | ); 27 | 28 | container.registerSingleton( 29 | 'UserTokensRepository', 30 | UserTokensRepository, 31 | ); 32 | 33 | container.registerSingleton( 34 | 'NotificationsRepository', 35 | NotificationsRepository, 36 | ); 37 | -------------------------------------------------------------------------------- /src/shared/container/providers/CacheProvider/implementations/RedisCacheProvider.ts: -------------------------------------------------------------------------------- 1 | import Redis, { Redis as IRedis } from 'ioredis'; 2 | import cacheConfig from '@config/cache'; 3 | import ICacheProvider from '../models/ICacheProvider'; 4 | 5 | export default class RedisCachePRovider implements ICacheProvider { 6 | private client: IRedis; 7 | 8 | constructor() { 9 | this.client = new Redis(cacheConfig.config.redis); 10 | } 11 | 12 | public async save(key: string, value: any): Promise { 13 | await this.client.set(key, JSON.stringify(value)); 14 | } 15 | 16 | public async recover(key: string): Promise { 17 | const data = await this.client.get(key); 18 | if (!data) { 19 | return null; 20 | } 21 | const parsedData = JSON.parse(data) as T; 22 | return parsedData; 23 | } 24 | 25 | public async invalidate(key: string): Promise { 26 | await this.client.del(key); 27 | } 28 | 29 | public async invalidatePrefix(prefix: string): Promise { 30 | const keys = this.client.keys(`${prefix}:*`); 31 | 32 | const pipeline = this.client.pipeline(); 33 | 34 | (await keys).forEach(key => { 35 | pipeline.del(key); 36 | }); 37 | 38 | await pipeline.exec(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/shared/container/providers/CacheProvider/implementations/fakes/FakeCacheProvider.ts: -------------------------------------------------------------------------------- 1 | import ICacheProvider from '../../models/ICacheProvider'; 2 | 3 | interface ICacheData { 4 | [key: string]: string; 5 | } 6 | 7 | export default class FakeCachePRovider implements ICacheProvider { 8 | private cache: ICacheData = {}; 9 | 10 | public async save(key: string, value: any): Promise { 11 | this.cache[key] = JSON.stringify(value); 12 | } 13 | 14 | public async recover(key: string): Promise { 15 | const data = this.cache[key]; 16 | if (!data) { 17 | return null; 18 | } 19 | const parsedData = JSON.parse(data) as T; 20 | return parsedData; 21 | } 22 | 23 | public async invalidate(key: string): Promise { 24 | delete this.cache[key]; 25 | } 26 | 27 | public async invalidatePrefix(prefix: string): Promise { 28 | const keys = Object.keys(this.cache).filter(key => 29 | key.startsWith(`${prefix}:`), 30 | ); 31 | 32 | keys.forEach(key => { 33 | delete this.cache[key]; 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/container/providers/CacheProvider/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from 'tsyringe'; 2 | import ICacheProvider from './models/ICacheProvider'; 3 | import RedisCacheProvider from './implementations/RedisCacheProvider'; 4 | 5 | const providers = { 6 | redis: RedisCacheProvider, 7 | }; 8 | 9 | container.registerSingleton('CacheProvider', providers.redis); 10 | -------------------------------------------------------------------------------- /src/shared/container/providers/CacheProvider/models/ICacheProvider.ts: -------------------------------------------------------------------------------- 1 | export default interface ICacheProvider { 2 | save(key: string, value: any): Promise; 3 | recover(key: string): Promise; 4 | invalidate(key: string): Promise; 5 | invalidatePrefix(prefix: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailProvider/dtos/ISendMailDTO.ts: -------------------------------------------------------------------------------- 1 | import IParseMailTemplateDTO from '@shared/container/providers/MailTemplateProvider/dtos/IParseMailTemplateDTO'; 2 | 3 | interface IMailContact { 4 | name: string; 5 | email: string; 6 | } 7 | 8 | export default interface ISendMailDTO { 9 | to: IMailContact; 10 | from?: IMailContact; 11 | subject: string; 12 | templateData: IParseMailTemplateDTO; 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailProvider/fakes/FakeMailProvider.ts: -------------------------------------------------------------------------------- 1 | import IMailProvider from '../models/IMailProvider'; 2 | import ISendMailDTO from '../dtos/ISendMailDTO'; 3 | 4 | export default class FakeMailProvider implements IMailProvider { 5 | private messages: ISendMailDTO[] = []; 6 | 7 | public async sendMail(message: ISendMailDTO): Promise { 8 | this.messages.push(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailProvider/implementations/CustomMailProvider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { inject, injectable } from 'tsyringe'; 3 | import nodemailer, { Transporter } from 'nodemailer'; 4 | import IMailProvider from '../models/IMailProvider'; 5 | import ISendMailDTO from '../dtos/ISendMailDTO'; 6 | import IMailTemplateProvider from '../../MailTemplateProvider/models/IMailTemplateProvider'; 7 | 8 | @injectable() 9 | export default class CustomMailProvider implements IMailProvider { 10 | private client: Transporter; 11 | 12 | // should setting email from gmail for sample via smtp 13 | constructor( 14 | @inject('MailTemplateProvider') 15 | private mailTemplateProvider: IMailTemplateProvider, 16 | ) { 17 | nodemailer.createTestAccount().then(account => { 18 | const transporter = nodemailer.createTransport({ 19 | host: 'account.smtp.host', 20 | port: account.smtp.port, 21 | secure: account.smtp.secure, 22 | auth: { 23 | user: account.user, 24 | pass: account.pass, 25 | }, 26 | }); 27 | 28 | this.client = transporter; 29 | }); 30 | } 31 | 32 | public async sendMail({ 33 | to, 34 | subject, 35 | from, 36 | templateData, 37 | }: ISendMailDTO): Promise { 38 | console.log(' custom working'); 39 | const message = await this.client.sendMail({ 40 | from: { 41 | name: from?.name || 'Equipe Gobarber', 42 | address: from?.email || 'equipe@gobarber.com.br', 43 | }, 44 | to: { name: to.name, address: to.email }, 45 | subject, 46 | html: await this.mailTemplateProvider.parse(templateData), 47 | }); 48 | 49 | console.log('Message sent: %s', message.messageId); 50 | console.log('Preview URL: %s', nodemailer.getTestMessageUrl(message)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailProvider/implementations/EtherealMailProvider.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { inject, injectable } from 'tsyringe'; 3 | import nodemailer, { Transporter } from 'nodemailer'; 4 | import IMailProvider from '../models/IMailProvider'; 5 | import ISendMailDTO from '../dtos/ISendMailDTO'; 6 | import IMailTemplateProvider from '../../MailTemplateProvider/models/IMailTemplateProvider'; 7 | 8 | @injectable() 9 | export default class EtherealMailProvider implements IMailProvider { 10 | private client: Transporter; 11 | 12 | constructor( 13 | @inject('MailTemplateProvider') 14 | private mailTemplateProvider: IMailTemplateProvider, 15 | ) { 16 | nodemailer.createTestAccount().then(account => { 17 | const transporter = nodemailer.createTransport({ 18 | host: account.smtp.host, 19 | port: account.smtp.port, 20 | secure: account.smtp.secure, 21 | auth: { 22 | user: account.user, 23 | pass: account.pass, 24 | }, 25 | }); 26 | 27 | this.client = transporter; 28 | }); 29 | } 30 | 31 | public async sendMail({ 32 | to, 33 | subject, 34 | from, 35 | templateData, 36 | }: ISendMailDTO): Promise { 37 | const message = await this.client.sendMail({ 38 | from: { 39 | name: from?.name || 'Equipe Gobarber', 40 | address: from?.email || 'equipe@gobarber.com.br', 41 | }, 42 | to: { name: to.name, address: to.email }, 43 | subject, 44 | html: await this.mailTemplateProvider.parse(templateData), 45 | }); 46 | 47 | console.log('Message sent: %s', message.messageId); 48 | console.log('Preview URL: %s', nodemailer.getTestMessageUrl(message)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailProvider/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from 'tsyringe'; 2 | import mailConfig from '@config/mail'; 3 | import IMailProvider from './models/IMailProvider'; 4 | 5 | import CustomMailProvider from './implementations/CustomMailProvider'; 6 | import EtherealMailProvider from './implementations/EtherealMailProvider'; 7 | 8 | const providers = { 9 | ethereal: container.resolve(EtherealMailProvider), 10 | custom: container.resolve(CustomMailProvider), 11 | }; 12 | 13 | container.registerInstance( 14 | 'MailProvider', 15 | providers[mailConfig.driver], 16 | ); 17 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailProvider/models/IMailProvider.ts: -------------------------------------------------------------------------------- 1 | import ISendMailDTO from '../dtos/ISendMailDTO'; 2 | 3 | export default interface IMailProvider { 4 | sendMail(data: ISendMailDTO): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailTemplateProvider/dtos/IParseMailTemplateDTO.ts: -------------------------------------------------------------------------------- 1 | interface ITemplateVariables { 2 | [key: string]: string | number; 3 | } 4 | 5 | export default interface IParseMailTemplateDTO { 6 | file: string; 7 | variables: ITemplateVariables; 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailTemplateProvider/fakes/FakeMailTemplateProvider.ts: -------------------------------------------------------------------------------- 1 | import IMailTemplateProvider from '../models/IMailTemplateProvider'; 2 | 3 | class FakeMailTemplateProvider implements IMailTemplateProvider { 4 | public async parse(): Promise { 5 | return 'Mail content'; 6 | } 7 | } 8 | 9 | export default FakeMailTemplateProvider; 10 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailTemplateProvider/implementations/HbsMailTemplateProvider.ts: -------------------------------------------------------------------------------- 1 | import handlebars from 'handlebars'; 2 | import fs from 'fs'; 3 | import IMailTemplateProvider from '../models/IMailTemplateProvider'; 4 | import IParseMailTemplateDTO from '../dtos/IParseMailTemplateDTO'; 5 | 6 | class HbsMailTemplateProvider implements IMailTemplateProvider { 7 | public async parse({ 8 | file, 9 | variables, 10 | }: IParseMailTemplateDTO): Promise { 11 | const templateFileContent = await fs.promises.readFile(file, { 12 | encoding: 'utf-8', 13 | }); 14 | const parseTemplate = handlebars.compile(templateFileContent); 15 | return parseTemplate(variables); 16 | } 17 | } 18 | 19 | export default HbsMailTemplateProvider; 20 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailTemplateProvider/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from 'tsyringe'; 2 | 3 | import IMailTemplateProvider from './models/IMailTemplateProvider'; 4 | 5 | import HbsMailTemplateProvider from './implementations/HbsMailTemplateProvider'; 6 | 7 | const providers = { 8 | handlebars: HbsMailTemplateProvider, 9 | }; 10 | 11 | container.registerSingleton( 12 | 'MailTemplateProvider', 13 | providers.handlebars, 14 | ); 15 | -------------------------------------------------------------------------------- /src/shared/container/providers/MailTemplateProvider/models/IMailTemplateProvider.ts: -------------------------------------------------------------------------------- 1 | import IParseMailTemplateDTO from '../dtos/IParseMailTemplateDTO'; 2 | 3 | export default interface IMailTemplateProvider { 4 | parse(data: IParseMailTemplateDTO): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/container/providers/StorageProvider/fakes/FakeStorageProvider.ts: -------------------------------------------------------------------------------- 1 | import IStorageProvider from '../models/IStorageProvider'; 2 | 3 | class FakeStorageProvider implements IStorageProvider { 4 | private storage: string[] = []; 5 | 6 | public async saveFile(file: string): Promise { 7 | this.storage.push(file); 8 | return file; 9 | } 10 | 11 | public async deleteFile(file: string): Promise { 12 | const findIndex = this.storage.findIndex( 13 | storageFile => storageFile === file, 14 | ); 15 | 16 | this.storage.splice(findIndex, 1); 17 | } 18 | } 19 | 20 | export default FakeStorageProvider; 21 | -------------------------------------------------------------------------------- /src/shared/container/providers/StorageProvider/implementations/DiskStorageProvider.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import uploadConfig from '@config/upload'; 4 | import IStorageProvider from '../models/IStorageProvider'; 5 | 6 | class DiskStorageProvider implements IStorageProvider { 7 | public async saveFile(file: string): Promise { 8 | await fs.promises.rename( 9 | path.resolve(uploadConfig.tempFolder, file), 10 | path.resolve(uploadConfig.uploadsFolder, file), 11 | ); 12 | return file; 13 | } 14 | 15 | public async deleteFile(file: string): Promise { 16 | const filePath = path.resolve(uploadConfig.uploadsFolder, file); 17 | 18 | try { 19 | await fs.promises.stat(filePath); 20 | } catch { 21 | return; 22 | } 23 | 24 | await fs.promises.unlink(filePath); 25 | } 26 | } 27 | 28 | export default DiskStorageProvider; 29 | -------------------------------------------------------------------------------- /src/shared/container/providers/StorageProvider/implementations/S3StorageProvider.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import mime from 'mime'; 4 | import uploadConfig from '@config/upload'; 5 | import aws, { S3 } from 'aws-sdk'; 6 | import IStorageProvider from '../models/IStorageProvider'; 7 | 8 | class S3StorageProvider implements IStorageProvider { 9 | private client: S3; 10 | 11 | constructor() { 12 | this.client = new aws.S3({ region: 'us-east-1' }); 13 | } 14 | 15 | public async saveFile(file: string): Promise { 16 | const originalPath = path.resolve(uploadConfig.tempFolder, file); 17 | 18 | const ContentType = mime.getType(originalPath); 19 | 20 | if (!ContentType) { 21 | throw new Error('File not found'); 22 | } 23 | 24 | const fileContent = await fs.promises.readFile(originalPath); 25 | 26 | await this.client 27 | .putObject({ 28 | Bucket: uploadConfig.config.aws.bucket, 29 | Key: file, 30 | ACL: 'public-read', 31 | Body: fileContent, 32 | ContentType, 33 | }) 34 | .promise(); 35 | 36 | await fs.promises.unlink(originalPath); 37 | 38 | return file; 39 | } 40 | 41 | public async deleteFile(file: string): Promise { 42 | await this.client 43 | .deleteObject({ 44 | Bucket: uploadConfig.config.aws.bucket, 45 | Key: file, 46 | }) 47 | .promise(); 48 | } 49 | } 50 | 51 | export default S3StorageProvider; 52 | -------------------------------------------------------------------------------- /src/shared/container/providers/StorageProvider/index.ts: -------------------------------------------------------------------------------- 1 | import { container } from 'tsyringe'; 2 | import uploadConfig from '@config/upload'; 3 | import IStorageProvider from './models/IStorageProvider'; 4 | import DiskStorageProvider from './implementations/DiskStorageProvider'; 5 | import S3StorageProvider from './implementations/S3StorageProvider'; 6 | 7 | const providers = { 8 | disk: DiskStorageProvider, 9 | s3: S3StorageProvider, 10 | }; 11 | 12 | container.registerSingleton( 13 | 'StorageProvider', 14 | providers[uploadConfig.driver], 15 | ); 16 | -------------------------------------------------------------------------------- /src/shared/container/providers/StorageProvider/models/IStorageProvider.ts: -------------------------------------------------------------------------------- 1 | export default interface IStorageProvider { 2 | saveFile(file: string): Promise; 3 | deleteFile(file: string): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/shared/container/providers/index.ts: -------------------------------------------------------------------------------- 1 | import './StorageProvider'; 2 | import './MailTemplateProvider'; 3 | import './MailProvider'; 4 | import './CacheProvider'; 5 | -------------------------------------------------------------------------------- /src/shared/errors/AppError.ts: -------------------------------------------------------------------------------- 1 | export default class AppError { 2 | public readonly message: string; 3 | 4 | public readonly statusCode: number; 5 | 6 | constructor(message: string, statusCode = 400) { 7 | this.message = message; 8 | this.statusCode = statusCode; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/infra/http/middlewares/rateLimiter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { RateLimiterRedis } from 'rate-limiter-flexible'; 3 | import redis from 'redis'; 4 | import AppError from '@shared/errors/AppError'; 5 | 6 | const redisClient = redis.createClient({ 7 | host: process.env.REDIS_HOST, 8 | port: Number(process.env.REDIS_PORT), 9 | password: process.env.REDIS_PASS || undefined, 10 | }); 11 | 12 | // TODO limitar requisição por tempo caso o usuário persista 5/1 requisições. 13 | const limiter = new RateLimiterRedis({ 14 | storeClient: redisClient, 15 | keyPrefix: 'ratelimit', 16 | points: 5, 17 | duration: 1, 18 | }); 19 | 20 | export default async function rateLimiter( 21 | request: Request, 22 | response: Response, 23 | next: NextFunction, 24 | ): Promise { 25 | try { 26 | await limiter.consume(request.ip); 27 | return next(); 28 | } catch (err) { 29 | throw new AppError('Too many requests', 429); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/shared/infra/http/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import appointmentsRouter from '@modules/appointments/infra/http/routes/appointments.routes'; 3 | import providersRouter from '@modules/appointments/infra/http/routes/providers.routes'; 4 | import usersRouter from '@modules/users/infra/http/routes/users.routes'; 5 | import sessionsRouter from '@modules/users/infra/http/routes/sessions.routes'; 6 | import passwordRouter from '@modules/users/infra/http/routes/password.routes'; 7 | import profileRouter from '@modules/users/infra/http/routes/profile.routes'; 8 | 9 | const routes = Router(); 10 | 11 | routes.use('/appointments', appointmentsRouter); 12 | routes.use('/providers', providersRouter); 13 | routes.use('/users', usersRouter); 14 | routes.use('/sessions', sessionsRouter); 15 | routes.use('/password', passwordRouter); 16 | routes.use('/profile', profileRouter); 17 | 18 | export default routes; 19 | -------------------------------------------------------------------------------- /src/shared/infra/http/server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import 'dotenv/config'; 3 | import cors from 'cors'; 4 | import express, { NextFunction, Request, Response } from 'express'; 5 | import 'express-async-errors'; 6 | import '@shared/infra/typeorm'; 7 | import '@shared/container'; 8 | import uploadConfig from '@config/upload'; 9 | import rateLimiter from '@shared/infra/http/middlewares/rateLimiter'; 10 | import AppError from '@shared/errors/AppError'; 11 | import { errors } from 'celebrate'; 12 | import routes from './routes'; 13 | 14 | const app = express(); 15 | 16 | app.use(rateLimiter); 17 | app.use(cors()); 18 | app.use(express.json()); 19 | app.use('/files', express.static(uploadConfig.uploadsFolder)); 20 | app.use(routes); 21 | app.use(errors()); 22 | 23 | app.use((err: Error, request: Request, response: Response, _: NextFunction) => { 24 | if (err instanceof AppError) { 25 | return response 26 | .status(err.statusCode) 27 | .json({ status: 'error', message: err.message }); 28 | } 29 | 30 | // eslint-disable-next-line no-console 31 | console.log(err); 32 | 33 | return response.status(500).json({ 34 | status: 'error', 35 | message: 'Internal server error', 36 | }); 37 | }); 38 | 39 | app.listen(3333, () => { 40 | // eslint-disable-next-line no-console 41 | console.log('🚀 Server started on port 3333!'); 42 | }); 43 | -------------------------------------------------------------------------------- /src/shared/infra/typeorm/index.ts: -------------------------------------------------------------------------------- 1 | import { createConnections } from 'typeorm'; 2 | 3 | // encontra o arquivo ormconfig.json e cria a conexão. (super mágico), 4 | // mas poderia configurar passando parametro nessa função. 5 | // melhor prática utilizar o arquivo externo para isso 6 | createConnections(); 7 | -------------------------------------------------------------------------------- /src/shared/infra/typeorm/migrations/1586877055453-CreateUsers.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | 3 | export default class CreateUsers1586877055453 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.createTable( 6 | new Table({ 7 | name: 'users', 8 | columns: [ 9 | { 10 | name: 'id', 11 | type: 'uuid', 12 | isPrimary: true, 13 | generationStrategy: 'uuid', 14 | default: 'uuid_generate_v4()', // generate uuid automaticaly 15 | }, 16 | { 17 | name: 'name', 18 | type: 'varchar', 19 | }, 20 | { 21 | name: 'email', 22 | type: 'varchar', 23 | isUnique: true, 24 | }, 25 | { 26 | name: 'password', 27 | type: 'varchar', 28 | }, 29 | { 30 | name: 'created_at', 31 | type: 'timestamp', 32 | default: 'now()', 33 | }, 34 | { 35 | name: 'updated_at', 36 | type: 'timestamp', 37 | default: 'now()', 38 | }, 39 | ], 40 | }), 41 | ); 42 | } 43 | 44 | public async down(queryRunner: QueryRunner): Promise { 45 | await queryRunner.dropTable('users'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/shared/infra/typeorm/migrations/1586888144285-CreateAppointments.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MigrationInterface, 3 | QueryRunner, 4 | Table, 5 | TableForeignKey, 6 | } from 'typeorm'; 7 | 8 | export default class CreateAppointments1586888144285 9 | implements MigrationInterface { 10 | public async up(queryRunner: QueryRunner): Promise { 11 | await queryRunner.createTable( 12 | new Table({ 13 | name: 'appointments', 14 | columns: [ 15 | { 16 | name: 'id', 17 | type: 'uuid', 18 | isPrimary: true, 19 | generationStrategy: 'uuid', 20 | default: 'uuid_generate_v4()', // generate uuid automaticaly 21 | }, 22 | { 23 | name: 'provider_id', 24 | type: 'uuid', 25 | isNullable: true, 26 | }, 27 | { 28 | name: 'date', 29 | type: 'timestamp', 30 | }, 31 | { 32 | name: 'created_at', 33 | type: 'timestamp', 34 | default: 'now()', 35 | }, 36 | { 37 | name: 'updated_at', 38 | type: 'timestamp', 39 | default: 'now()', 40 | }, 41 | ], 42 | }), 43 | ); 44 | 45 | await queryRunner.createForeignKey( 46 | 'appointments', 47 | new TableForeignKey({ 48 | name: 'AppointmentProviderFK', 49 | columnNames: ['provider_id'], 50 | referencedColumnNames: ['id'], 51 | referencedTableName: 'users', 52 | onDelete: 'SET NULL', 53 | onUpdate: 'CASCADE', 54 | }), 55 | ); 56 | } 57 | 58 | public async down(queryRunner: QueryRunner): Promise { 59 | await queryRunner.dropForeignKey('appointments', 'AppointmentProviderFK'); 60 | await queryRunner.dropTable('appointments'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/shared/infra/typeorm/migrations/1586981197413-AddAvatarFieldToUsers.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; 2 | 3 | export default class AddAvatarFieldToUsers1586981197413 4 | implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.addColumn( 7 | 'users', 8 | new TableColumn({ 9 | name: 'avatar', 10 | type: 'varchar', 11 | isNullable: true, 12 | }), 13 | ); 14 | } 15 | 16 | public async down(queryRunner: QueryRunner): Promise { 17 | await queryRunner.dropColumn('users', 'avatar'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/infra/typeorm/migrations/1588899359097-CreateUserTokens.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from 'typeorm'; 2 | 3 | export default class CreateUserTokens1588899004268 4 | implements MigrationInterface { 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.createTable( 7 | new Table({ 8 | name: 'user_tokens', 9 | columns: [ 10 | { 11 | name: 'id', 12 | type: 'uuid', 13 | isPrimary: true, 14 | generationStrategy: 'uuid', 15 | default: 'uuid_generate_v4()', // generate uuid automaticaly 16 | }, 17 | { 18 | name: 'token', 19 | type: 'uuid', 20 | generationStrategy: 'uuid', 21 | default: 'uuid_generate_v4()', // generate uuid automaticaly 22 | }, 23 | { 24 | name: 'user_id', 25 | type: 'uuid', 26 | }, 27 | { 28 | name: 'created_at', 29 | type: 'timestamp', 30 | default: 'now()', 31 | }, 32 | { 33 | name: 'updated_at', 34 | type: 'timestamp', 35 | default: 'now()', 36 | }, 37 | ], 38 | foreignKeys: [ 39 | { 40 | name: 'TokenUser', 41 | referencedTableName: 'users', 42 | referencedColumnNames: ['id'], 43 | columnNames: ['user_id'], 44 | onDelete: 'CASCADE', 45 | onUpdate: 'CASCADE', 46 | }, 47 | ], 48 | }), 49 | ); 50 | } 51 | 52 | public async down(queryRunner: QueryRunner): Promise { 53 | await queryRunner.dropTable('user_tokens'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/shared/infra/typeorm/migrations/1589120070157-AddUserIdToAppointment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MigrationInterface, 3 | QueryRunner, 4 | TableColumn, 5 | TableForeignKey, 6 | } from 'typeorm'; 7 | 8 | export default class AddUserIdToAppointment1589120070157 9 | implements MigrationInterface { 10 | public async up(queryRunner: QueryRunner): Promise { 11 | await queryRunner.addColumn( 12 | 'appointments', 13 | new TableColumn({ 14 | name: 'user_id', 15 | type: 'uuid', 16 | isNullable: true, 17 | }), 18 | ); 19 | 20 | await queryRunner.createForeignKey( 21 | 'appointments', 22 | new TableForeignKey({ 23 | name: 'AppointmentUser', 24 | columnNames: ['user_id'], 25 | referencedColumnNames: ['id'], 26 | referencedTableName: 'users', 27 | onDelete: 'SET NULL', 28 | onUpdate: 'CASCADE', 29 | }), 30 | ); 31 | } 32 | 33 | public async down(queryRunner: QueryRunner): Promise { 34 | await queryRunner.dropForeignKey('appointments', 'AppointmentUser'); 35 | await queryRunner.dropColumn('appointments', 'user_id'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /temp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgmarinho/gobarber-api-gostack11/6b4b30121a044ce442f8dca7e404ef687190218c/temp/.gitkeep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "allowJs": true, 9 | "strictPropertyInitialization": false, 10 | "baseUrl": "./src", 11 | "paths": { 12 | "@modules/*": ["modules/*"], 13 | "@config/*": ["config/*"], 14 | "@shared/*": ["shared/*"] 15 | }, 16 | "esModuleInterop": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "forceConsistentCasingInFileNames": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------