├── .commitlintrc.json ├── .env.example ├── .env.example-test-e2e ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── nodejs-environment.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierrc ├── LICENSE ├── README.md ├── docker-compose.yml ├── eslint.config.mjs ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── renovate.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.spec.ts ├── app.service.ts ├── common │ ├── hashing │ │ ├── argon2.service.spec.ts │ │ ├── argon2.service.ts │ │ ├── hashing.service.spec.ts │ │ └── hashing.service.ts │ ├── logger │ │ └── logger.service.ts │ ├── mailer │ │ ├── mailer.constants.ts │ │ ├── mailer.module.ts │ │ ├── mailer.service.spec.ts │ │ └── mailer.service.ts │ ├── plugins │ │ └── register-fastify.plugins.ts │ └── utils │ │ ├── utils.module.ts │ │ ├── utils.service.spec.ts │ │ └── utils.service.ts ├── constants.ts ├── helpers │ ├── configure-auth-swagger-docs.helper.ts │ ├── configure-swagger-docs.helper.ts │ └── validation-schema-env.ts ├── iam │ ├── change-password │ │ ├── change-password.controller.spec.ts │ │ ├── change-password.controller.ts │ │ ├── change-password.module.ts │ │ ├── change-password.service.spec.ts │ │ ├── change-password.service.ts │ │ └── dto │ │ │ └── change-password.dto.ts │ ├── forgot-password │ │ ├── dto │ │ │ └── forgot-password.dto.ts │ │ ├── forgot-password.controller.spec.ts │ │ ├── forgot-password.controller.ts │ │ ├── forgot-password.module.ts │ │ ├── forgot-password.service.spec.ts │ │ └── forgot-password.service.ts │ ├── iam.constants.ts │ ├── iam.module.ts │ ├── login │ │ ├── config │ │ │ └── jwt.config.ts │ │ ├── decorators │ │ │ └── auth-guard.decorator.ts │ │ ├── dto │ │ │ ├── login.dto.ts │ │ │ └── refresh-token.dto.ts │ │ ├── enums │ │ │ └── auth-type.enum.ts │ │ ├── guards │ │ │ ├── access-token │ │ │ │ └── access-token.guard.ts │ │ │ └── authentication │ │ │ │ └── authentication.guard.ts │ │ ├── interfaces │ │ │ ├── auth-response.interface.ts │ │ │ └── jwt-payload.interface.ts │ │ ├── login.controller.spec.ts │ │ ├── login.controller.ts │ │ ├── login.module.ts │ │ ├── login.service.spec.ts │ │ └── login.service.ts │ └── register │ │ ├── dto │ │ └── register-user.dto.ts │ │ ├── register.controller.spec.ts │ │ ├── register.controller.ts │ │ ├── register.module.ts │ │ ├── register.service.spec.ts │ │ └── register.service.ts ├── main.ts ├── migrations │ └── 1589834500772-Api.ts ├── repl.ts └── users │ ├── dto │ ├── user-profile.dto.ts │ ├── user-update.dto.ts │ └── user.dto.ts │ ├── entities │ ├── user.entity.spec.ts │ └── user.entity.ts │ ├── interfaces │ └── accounts-users.interface.ts │ ├── models │ └── users.model.ts │ ├── repositories │ ├── implementations │ │ └── users.typeorm.repository.ts │ ├── users.repository.interface.ts │ └── users.repository.provider.ts │ ├── users.controller.spec.ts │ ├── users.controller.ts │ ├── users.module.ts │ ├── users.service.spec.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts ├── change-password │ └── change-password.e2e-spec.ts ├── forgot-password │ └── forgot-password.e2e-spec.ts ├── jest-e2e.json ├── login │ └── login.e2e-spec.ts ├── register │ └── register.e2e-spec.ts └── users │ └── users.e2e-spec.ts ├── tsconfig.build.json ├── tsconfig.json └── typeorm-cli.config.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "always", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV="production" 2 | 3 | SERVER_PORT= 4 | 5 | ENDPOINT_URL_CORS= 6 | 7 | SWAGGER_USER= 8 | SWAGGER_PASSWORD= 9 | 10 | EMAIL_HOST="smtp.mailtrap.io" 11 | EMAIL_PORT=2525 12 | EMAIL_AUTH_USER="" 13 | EMAIL_AUTH_PASSWORD="" 14 | EMAIL_DEBUG=true 15 | EMAIL_LOGGER=true 16 | 17 | JWT_SECRET_KEY="secretOrKey" 18 | JWT_TOKEN_AUDIENCE="localhost:3000" 19 | JWT_TOKEN_ISSUER="localhost:3000" 20 | JWT_ACCESS_TOKEN_TTL=3600 21 | JWT_REFRESH_TOKEN_TTL=86400 22 | 23 | TYPEORM_CONNECTION="mysql" 24 | TYPEORM_HOST="localhost" 25 | TYPEORM_PORT=3306 26 | TYPEORM_USERNAME="" 27 | TYPEORM_PASSWORD="" 28 | TYPEORM_DATABASE="" 29 | TYPEORM_AUTO_SCHEMA_SYNC=true 30 | TYPEORM_ENTITIES="dist/**/*.entity.js" 31 | TYPEORM_SUBSCRIBERS="dist/subscriber/**/*.js" 32 | TYPEORM_MIGRATIONS="dist/migrations/**/*.js" 33 | TYPEORM_ENTITIES_DIR="src/entity" 34 | TYPEORM_MIGRATIONS_DIR="src/migrations" 35 | TYPEORM_SUBSCRIBERS_DIR="src/subscriber" 36 | 37 | USERS_DATASOURCE=typeorm 38 | -------------------------------------------------------------------------------- /.env.example-test-e2e: -------------------------------------------------------------------------------- 1 | NODE_ENV="production" 2 | 3 | SERVER_PORT= 4 | 5 | ENDPOINT_URL_CORS= 6 | 7 | SWAGGER_USER= 8 | SWAGGER_PASSWORD= 9 | 10 | EMAIL_HOST="smtp.mailtrap.io" 11 | EMAIL_PORT=2525 12 | EMAIL_AUTH_USER="" 13 | EMAIL_AUTH_PASSWORD="" 14 | EMAIL_DEBUG=true 15 | EMAIL_LOGGER=true 16 | 17 | JWT_SECRET_KEY="secretOrKey" 18 | JWT_TOKEN_AUDIENCE="127.0.0.1:3001" 19 | JWT_TOKEN_ISSUER="127.0.0.1:3001" 20 | JWT_ACCESS_TOKEN_TTL=3600 21 | 22 | TYPEORM_CONNECTION="mysql" 23 | TYPEORM_HOST="localhost" 24 | TYPEORM_PORT=3307 25 | TYPEORM_USERNAME="root" 26 | TYPEORM_PASSWORD="root" 27 | TYPEORM_DATABASE="test" 28 | TYPEORM_AUTO_SCHEMA_SYNC = true 29 | TYPEORM_ENTITIES="dist/**/*.entity.js" 30 | TYPEORM_SUBSCRIBERS="dist/subscriber/**/*.js" 31 | TYPEORM_MIGRATIONS="dist/migrations/**/*.js" 32 | TYPEORM_ENTITIES_DIR="src/entity" 33 | TYPEORM_MIGRATIONS_DIR="src/migrations" 34 | TYPEORM_SUBSCRIBERS_DIR="src/subscriber" 35 | 36 | USERS_DATASOURCE=typeorm 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | labels: 10 | - dependabot 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 0 * * *' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Initialize CodeQL 23 | uses: github/codeql-action/init@v3 24 | with: 25 | languages: javascript 26 | 27 | - name: Autobuild 28 | uses: github/codeql-action/autobuild@v3 29 | 30 | - name: Perform CodeQL Analysis 31 | uses: github/codeql-action/analyze@v3 32 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-environment.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x, 22.x, 24.x] 16 | needs: [build] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | name: Checkout code 21 | - name: Set safe.directory 22 | run: git config --global --add safe.directory /github/workspace 23 | - uses: pnpm/action-setup@v4 24 | with: 25 | version: 8 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'pnpm' 30 | - name: Run unit test 31 | run: | 32 | pnpm install 33 | pnpm test 34 | 35 | e2e-test: 36 | runs-on: ubuntu-latest 37 | 38 | strategy: 39 | matrix: 40 | node-version: [20.x, 22.x, 24.x] 41 | needs: [unit-tests] 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | name: Checkout code 46 | - name: Set safe.directory 47 | run: git config --global --add safe.directory /github/workspace 48 | - uses: actions/setup-node@v4 49 | with: 50 | node-version: ${{ matrix.node-version }} 51 | cache: 'pnpm' 52 | - uses: pnpm/action-setup@v4 53 | with: 54 | version: 8 55 | - name: Install Docker Compose 56 | run: | 57 | sudo apt-get update 58 | sudo apt-get install -y docker-compose 59 | - name: Start Docker-Compose 60 | run: docker-compose up -d db-test 61 | - name: Install pnpm dependencies 62 | run: pnpm install 63 | - run: cp .env.example-test-e2e .env 64 | - name: Run tests e2e -- register 65 | run: pnpm test:e2e -- register.e2e-spec.ts 66 | - name: Run tests e2e -- login 67 | run: pnpm test:e2e -- login.e2e-spec.ts 68 | - name: Run tests e2e -- hello 69 | run: pnpm test:e2e -- app.e2e-spec.ts 70 | - name: Run tests e2e -- change-password 71 | run: pnpm test:e2e -- change-password.e2e-spec.ts 72 | - name: Run tests e2e -- users 73 | run: pnpm test:e2e -- users.e2e-spec.ts 74 | - name: Run tests e2e -- forgot-password 75 | run: pnpm test:e2e -- forgot-password.e2e-spec.ts 76 | - name: Stop Docker-Compose 77 | run: docker-compose down 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Enviroment 6 | .env 7 | .env.dev 8 | .env.stage 9 | .env.prod 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | 19 | # OS 20 | .DS_Store 21 | 22 | # Tests 23 | /coverage 24 | /.nyc_output 25 | 26 | # IDEs and editors 27 | /.idea 28 | .project 29 | .classpath 30 | .c9/ 31 | *.launch 32 | .settings/ 33 | *.sublime-workspace 34 | tags 35 | 36 | # IDE - VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx --no-install commitlint --edit $1 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm test 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 80 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 Tony133 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | # NestJSApiBoilerplateJWT 6 | 7 | An API Boilerplate to create a ready-to-use REST API in seconds with NestJS 11.x and JWT Auth System :heart_eyes_cat: 8 | 9 | ## Installation 10 | 11 | ```bash 12 | pnpm install 13 | ``` 14 | 15 | ## Set Environment for secret key JWT and other configurations 16 | 17 | ```bash 18 | cp .env.example .env 19 | ``` 20 | 21 | To set up on multiple environments, such as development, staging or production, we do as follows: 22 | 23 | ```bash 24 | cp .env.example .env.development # or .env.staging, etc 25 | ``` 26 | 27 | ## Config settings .env for sending a notification when a user registers, forgets password or changes password 28 | 29 | ``` 30 | EMAIL_HOST=smtp.mailtrap.io 31 | EMAIL_PORT=2525 32 | EMAIL_AUTH_USER=[:user] 33 | EMAIL_AUTH_PASSWORD=[:password] 34 | EMAIL_DEBUG=true 35 | EMAIL_LOGGER=true 36 | ``` 37 | 38 | ## Config settings .env to connect MySQL 39 | 40 | Once the database has been configured, start the Nest App via `pnpm run start:dev` it automatically synchronizes the entities so it is ready to use. 41 | 42 | ``` 43 | TYPEORM_CONNECTION = "mysql" 44 | TYPEORM_HOST = "localhost" 45 | TYPEORM_PORT = 3306 46 | TYPEORM_USERNAME = [:user] 47 | TYPEORM_PASSWORD = [:password] 48 | TYPEORM_DATABASE = [:database] 49 | TYPEORM_AUTO_SCHEMA_SYNC = true 50 | TYPEORM_ENTITIES = "dist/**/*.entity.js" 51 | TYPEORM_SUBSCRIBERS = "dist/subscriber/**/*.js" 52 | TYPEORM_MIGRATIONS = "dist/migrations/**/*.js" 53 | TYPEORM_ENTITIES_DIR = "src/entity" 54 | TYPEORM_MIGRATIONS_DIR = "src/migration" 55 | TYPEORM_SUBSCRIBERS_DIR = "src/subscriber" 56 | ``` 57 | 58 | ## Install TypeScript Node 59 | 60 | ```bash 61 | pnpm install -g ts-node 62 | ``` 63 | 64 | ## Running migrations with typeorm 65 | 66 | ```bash 67 | ts-node node_modules/.bin/typeorm migration:run -d dist/typeorm-cli.config 68 | ``` 69 | 70 | or 71 | 72 | ```bash 73 | node_modules/.bin/typeorm migration:run -d dist/typeorm-cli.config 74 | ``` 75 | 76 | ## Running the app 77 | 78 | ```bash 79 | # development 80 | $ pnpm start 81 | 82 | # watch mode 83 | $ pnpm start:dev 84 | 85 | # production mode 86 | $ pnpm start:prod 87 | ``` 88 | 89 | ## Running the app in REPL mode 90 | 91 | ```bash 92 | pnpm start --entryFile repl 93 | ``` 94 | 95 | or 96 | 97 | ```bash 98 | pnpm start:repl 99 | ``` 100 | 101 | ## Docker 102 | 103 | There is a `docker-compose.yml` file for starting MySQL with Docker. 104 | 105 | `$ docker-compose up db` 106 | 107 | After running, you can stop the Docker container with 108 | 109 | `$ docker-compose down` 110 | 111 | ## Url Swagger for Api Documentation 112 | 113 | ``` 114 | 115 | http://127.0.0.1:3000/docs 116 | 117 | ``` 118 | 119 | or 120 | 121 | ``` 122 | 123 | http://127.0.0.1:3000/docs-json 124 | 125 | ``` 126 | 127 | or 128 | 129 | ``` 130 | 131 | http://127.0.0.1:3000/docs-yaml 132 | 133 | ``` 134 | 135 | Configure `SWAGGER_USER` and `SWAGGER_PASSWORD` in the .env file for to access the Swagger(Open API) documentation with basic authentication. `NODE_ENV` 136 | must not be equal to "production" otherwise the Swagger is not displayed. 137 | 138 | ``` 139 | NODE_ENV=[:enviroments] 140 | SWAGGER_USER=[:user] 141 | SWAGGER_PASSWORD=[:password] 142 | 143 | ``` 144 | 145 | ## Configuring the SERVER_PORT environment variable as the default port if you don't want to use the default 146 | 147 | ``` 148 | SERVER_PORT=3333 149 | ``` 150 | 151 | ## Configuring the ENDPOINT_URL_CORS environment variable for app frontend 152 | 153 | ``` 154 | ENDPOINT_URL_CORS='http://127.0.0.1:4200' 155 | ``` 156 | 157 | ## Getting secure resource with Curl 158 | 159 | ```bash 160 | curl -H 'content-type: application/json' -v -X GET http://127.0.0.1:3000/api/secure -H 'Authorization: Bearer [:token]' 161 | ``` 162 | 163 | ## Generate Token JWT Authentication with Curl 164 | 165 | ```bash 166 | curl -H 'content-type: application/json' -v -X POST -d '{"email": "tony_admin@nest.com", "password": "mysecret"}' http://127.0.0.1:3000/api/auth/login 167 | ``` 168 | 169 | ## Registration user with Curl 170 | 171 | ```bash 172 | curl -H 'content-type: application/json' -v -X POST -d '{"name": "tony", "email": "tony_admin@nest.com", "username":"tony_admin", "password": "mysecret"}' http://127.0.0.1:3000/api/auth/register 173 | ``` 174 | 175 | ## Refresh token with curl 176 | 177 | ```bash 178 | curl -H 'content-type: application/json' -v -X POST -d '{"refreshToken": "[:token]"}' http://127.0.0.1:3000/api/auth/refresh-tokens 179 | ``` 180 | 181 | ## Forgot password with curl 182 | 183 | ```bash 184 | curl -H 'content-type: application/json' -v -X POST -d '{"email": "tony_admin@nest.com"}' http://127.0.0.1:3000/api/auth/forgot-password 185 | ``` 186 | 187 | ## Change password User with curl 188 | 189 | ```bash 190 | curl -H 'content-type: application/json' -v -X POST -d '{"email": "tony_admin@nest.com", "password": "new_password"}' http://127.0.0.1:3000/api/auth/change-password -H 'Authorization: Bearer [:token]' 191 | ``` 192 | 193 | ## Update profile User with curl 194 | 195 | ```bash 196 | curl -H 'content-type: application/json' -v -X PUT -d '{"name": "tony", "email": "tony_admin@nest.com", "username": "tony_admin"}' http://127.0.0.1:3000/api/users/:id/profile -H 'Authorization: Bearer [:token]' 197 | ``` 198 | 199 | ## Users list with Curl 200 | 201 | ```bash 202 | curl -H 'content-type: application/json' -H 'Accept: application/json' -v -X GET http://127.0.0.1:3000/api/users -H 'Authorization: Bearer [:token]' 203 | ``` 204 | 205 | ## User by Id with Curl 206 | 207 | ```bash 208 | curl -H 'content-type: application/json' -H 'Accept: application/json' -v -X GET http://127.0.0.1:3000/api/users/:id -H 'Authorization: Bearer [:token]' 209 | ``` 210 | 211 | ## Update User with Curl 212 | 213 | ```bash 214 | curl -H 'content-type: application/json' -v -X PUT -d '{"name": "tony", "email": "tony_admin@nest.com", "username": "tony_admin", "password":"password_update"}' http://127.0.0.1:3000/api/users/:id -H 'Authorization: Bearer [:token]' 215 | ``` 216 | 217 | ## Delete User by Id with Curl 218 | 219 | ```bash 220 | curl -H 'content-type: application/json' -H 'Accept: application/json' -v -X DELETE http://127.0.0.1:3000/api/users/:id -H 'Authorization: Bearer [:token]' 221 | ``` 222 | 223 | ## License 224 | 225 | [MIT licensed](LICENSE) 226 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: mysql:8.3 4 | restart: always 5 | environment: 6 | MYSQL_ROOT_PASSWORD: root 7 | MYSQL_DATABASE: nest 8 | ports: 9 | - '3306:3306' 10 | 11 | db-test: 12 | image: mysql:8.3 13 | restart: always 14 | environment: 15 | MYSQL_ROOT_PASSWORD: root 16 | MYSQL_DATABASE: test 17 | ports: 18 | - '3307:3306' 19 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | { 9 | ignores: ['eslint.config.mjs'], 10 | }, 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommendedTypeChecked, 13 | eslintPluginPrettierRecommended, 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.node, 18 | ...globals.jest, 19 | }, 20 | ecmaVersion: 5, 21 | sourceType: 'module', 22 | parserOptions: { 23 | projectService: true, 24 | tsconfigRootDir: import.meta.dirname, 25 | }, 26 | }, 27 | }, 28 | { 29 | rules: { 30 | '@typescript-eslint/no-explicit-any': 'off', 31 | '@typescript-eslint/no-floating-promises': 'off', 32 | '@typescript-eslint/no-require-imports': 'off', 33 | '@typescript-eslint/no-unsafe-argument': 'off', 34 | '@typescript-eslint/ban-ts-comment': 'off', 35 | '@typescript-eslint/no-unsafe-return': 'off', 36 | '@typescript-eslint/no-unsafe-assignment': 'off', 37 | '@typescript-eslint/no-unsafe-call': 'warn', 38 | '@typescript-eslint/no-unsafe-member-access': 'warn', 39 | '@typescript-eslint/require-await': 'warn', 40 | '@typescript-eslint/no-unused-vars': 'warn' 41 | }, 42 | }, 43 | ); 44 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "deleteOutDir": true, 6 | "plugins": ["@nestjs/swagger"] 7 | } 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-api-boilerplate-jwt", 3 | "version": "1.0.0", 4 | "description": "An API Boilerplate to create a ready-to-use REST API in seconds with NestJS 11.x and Auth JWT System", 5 | "author": "Tony133", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "start:repl": "nest start --entryFile repl", 16 | "lint": "eslint {src,test}/**/*.ts --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json", 22 | "prepare": "husky" 23 | }, 24 | "dependencies": { 25 | "@fastify/cors": "^11.0.1", 26 | "@fastify/helmet": "^13.0.1", 27 | "@fastify/rate-limit": "^10.3.0", 28 | "@nestjs/common": "^11.1.2", 29 | "@nestjs/core": "^11.1.2", 30 | "@nestjs/jwt": "^11.0.0", 31 | "@nestjs/mapped-types": "2.1.0", 32 | "@nestjs/platform-fastify": "^11.1.2", 33 | "@nestjs/swagger": "^11.2.0", 34 | "@nestjs/typeorm": "^11.0.0", 35 | "ajv": "^8.17.1", 36 | "argon2": "^0.43.0", 37 | "class-transformer": "^0.5.1", 38 | "class-validator": "^0.14.2", 39 | "dotenv": "^16.5.0", 40 | "fastify": "^5.3.3", 41 | "mysql2": "^3.14.1", 42 | "nodemailer": "^6.10.1", 43 | "reflect-metadata": "^0.2.2", 44 | "rimraf": "^6.0.1", 45 | "rxjs": "^7.8.2", 46 | "typeorm": "^0.3.24" 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "^19.8.1", 50 | "@commitlint/config-angular": "^19.8.1", 51 | "@eslint/eslintrc": "3.3.1", 52 | "@eslint/js": "9.28.0", 53 | "@nestjs/cli": "^11.0.7", 54 | "@nestjs/schematics": "^11.0.5", 55 | "@nestjs/testing": "^11.1.2", 56 | "@types/jest": "^29.5.14", 57 | "@types/node": "^22.15.29", 58 | "@types/nodemailer": "^6.4.17", 59 | "@types/supertest": "^6.0.3", 60 | "eslint": "9.28.0", 61 | "eslint-config-prettier": "^10.1.5", 62 | "eslint-plugin-prettier": "^5.4.1", 63 | "globals": "16.2.0", 64 | "husky": "^9.1.7", 65 | "jest": "^29.7.0", 66 | "pino-pretty": "^13.0.0", 67 | "prettier": "^3.5.3", 68 | "supertest": "^7.1.1", 69 | "ts-jest": "^29.3.4", 70 | "ts-loader": "^9.5.2", 71 | "ts-node": "^10.9.2", 72 | "tsconfig-paths": "^4.2.0", 73 | "typescript": "^5.8.3", 74 | "typescript-eslint": "8.34.0" 75 | }, 76 | "jest": { 77 | "moduleFileExtensions": [ 78 | "js", 79 | "json", 80 | "ts" 81 | ], 82 | "rootDir": "src", 83 | "testRegex": ".spec.ts$", 84 | "transform": { 85 | "^.+\\.(t|j)s$": "ts-jest" 86 | }, 87 | "collectCoverageFrom": [ 88 | "**/*.{!(module),}.(t|j)s" 89 | ], 90 | "coveragePathIgnorePatterns": [ 91 | "/src/migrations", 92 | "/src/helpers", 93 | "/src/main.ts", 94 | "/src/repl.ts", 95 | "/src/shared/logger", 96 | "/src/users/repositories", 97 | ".constants.ts", 98 | ".guard.ts", 99 | ".config.ts" 100 | ], 101 | "coverageDirectory": "../coverage", 102 | "testEnvironment": "node" 103 | }, 104 | "lint-staged": { 105 | "**/*.{ts,json}": [] 106 | }, 107 | "pnpm": { 108 | "onlyBuiltDependencies": [ 109 | "@nestjs/core", 110 | "@scarf/scarf", 111 | "argon2" 112 | ] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "packageRules": [{ 4 | "depTypeList": ["devDependencies"], 5 | "automerge": true 6 | }], 7 | "extends": [ 8 | "config:base" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | let appService: AppService; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [ 13 | { 14 | provide: AppService, 15 | useValue: { 16 | getHello: jest.fn(() => {}), 17 | getSecureResource: jest.fn(() => {}), 18 | }, 19 | }, 20 | ], 21 | }).compile(); 22 | 23 | appController = app.get(AppController); 24 | appService = app.get(AppService); 25 | }); 26 | 27 | describe('root', () => { 28 | it('should be defined', () => { 29 | expect(appController).toBeDefined(); 30 | }); 31 | 32 | it('should call method getHello() in AppService', () => { 33 | const createSpy = jest.spyOn(appService, 'getHello'); 34 | 35 | appController.getHello(); 36 | expect(createSpy).toHaveBeenCalled(); 37 | }); 38 | 39 | it('should call method getProtectedResource() in AppService', () => { 40 | const createSpy = jest.spyOn(appService, 'getSecureResource'); 41 | 42 | appController.getProtectedResource(); 43 | expect(createSpy).toHaveBeenCalled(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { AuthGuard } from './iam/login/decorators/auth-guard.decorator'; 4 | import { AuthType } from './iam/login/enums/auth-type.enum'; 5 | import { ApiOkResponse, ApiBearerAuth, ApiTags } from '@nestjs/swagger'; 6 | 7 | @ApiTags('app') 8 | @Controller() 9 | export class AppController { 10 | constructor(private readonly appService: AppService) {} 11 | 12 | @AuthGuard(AuthType.None) 13 | @Get() 14 | @ApiOkResponse({ 15 | description: 'Example of a public resource', 16 | }) 17 | getHello(): { message: string } { 18 | return this.appService.getHello(); 19 | } 20 | 21 | @AuthGuard(AuthType.Bearer) 22 | @Get('secure') 23 | @ApiBearerAuth() 24 | @ApiOkResponse({ 25 | description: 'Example of a protected resource', 26 | }) 27 | getProtectedResource(): { message: string } { 28 | return this.appService.getSecureResource(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { UsersModule } from './users/users.module'; 6 | import { IamModule } from './iam/iam.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | TypeOrmModule.forRootAsync({ 11 | useFactory: () => ({ 12 | type: 'mysql', 13 | host: process.env.TYPEORM_HOST, 14 | port: process.env.TYPEORM_PORT 15 | ? parseInt(process.env.TYPEORM_PORT, 10) 16 | : 3306, 17 | username: process.env.TYPEORM_USERNAME, 18 | password: process.env.TYPEORM_PASSWORD, 19 | database: process.env.TYPEORM_DATABASE, 20 | synchronize: true, 21 | entities: [__dirname + '/**/*.{model,entity}.{ts,js}'], 22 | migrations: ['dist/migrations/**/*.js'], 23 | subscribers: ['dist/subscriber/**/*.js'], 24 | cli: { 25 | migrationsDir: process.env.TYPEORM_MIGRATIONS_DIR, 26 | subscribersDir: process.env.TYPEORM_SUBSCRIBERS_DIR, 27 | }, 28 | }), 29 | }), 30 | IamModule, 31 | UsersModule, 32 | ], 33 | controllers: [AppController], 34 | providers: [AppService], 35 | }) 36 | export class AppModule {} 37 | -------------------------------------------------------------------------------- /src/app.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppService } from './app.service'; 3 | 4 | describe('AppService', () => { 5 | let service: AppService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AppService], 10 | }).compile(); 11 | 12 | service = module.get(AppService); 13 | }); 14 | 15 | describe('App service', () => { 16 | it('should be defined', () => { 17 | expect(service).toBeDefined(); 18 | }); 19 | 20 | describe('getHello() method', () => { 21 | it('should return message "This is a simple example of item returned by your APIs"', () => { 22 | expect(service.getHello()).toEqual({ 23 | message: 'This is a simple example of item returned by your APIs.', 24 | }); 25 | }); 26 | }); 27 | 28 | describe('getSecureResource() method', () => { 29 | it('should return message "Access to protected resources granted! This protected resource is displayed when the token is successfully provided"', () => { 30 | expect(service.getSecureResource()).toEqual({ 31 | message: 32 | 'Access to protected resources granted! This protected resource is displayed when the token is successfully provided.', 33 | }); 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): { message: string } { 6 | return { 7 | message: 'This is a simple example of item returned by your APIs.', 8 | }; 9 | } 10 | 11 | getSecureResource(): { message: string } { 12 | return { 13 | message: 14 | 'Access to protected resources granted! This protected resource is displayed when the token is successfully provided.', 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/common/hashing/argon2.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Argon2Service } from './argon2.service'; 3 | 4 | describe('Argon2Service', () => { 5 | let service: Argon2Service; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ 10 | { 11 | provide: Argon2Service, 12 | useValue: { 13 | hash: jest.fn(), 14 | compare: jest.fn(() => true), 15 | }, 16 | }, 17 | ], 18 | }).compile(); 19 | 20 | service = module.get(Argon2Service); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(service).toBeDefined(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/common/hashing/argon2.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as argon2 from 'argon2'; 3 | import { HashingService } from './hashing.service'; 4 | 5 | @Injectable() 6 | export class Argon2Service implements HashingService { 7 | private readonly options = { 8 | type: argon2.argon2id, 9 | memoryCost: 2 ** 16, 10 | timeCost: 3, 11 | parallelism: 1, 12 | }; 13 | 14 | public async hash(data: string | Buffer): Promise { 15 | return await argon2.hash(data, this.options); 16 | } 17 | 18 | public compare(data: string | Buffer, encrypted: string): Promise { 19 | return argon2.verify(encrypted, data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/common/hashing/hashing.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Argon2Service } from './argon2.service'; 3 | import { HashingService } from './hashing.service'; 4 | 5 | describe('HashingService', () => { 6 | let service: HashingService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [ 11 | { 12 | provide: HashingService, 13 | useClass: Argon2Service, 14 | }, 15 | ], 16 | }).compile(); 17 | 18 | service = module.get(HashingService); 19 | }); 20 | 21 | it('should be defined', () => { 22 | expect(service).toBeDefined(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/common/hashing/hashing.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export abstract class HashingService { 5 | abstract hash(data: string | Buffer): Promise; 6 | abstract compare(data: string | Buffer, encrypted: string): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/logger/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Scope, LoggerService as LoggerBase } from '@nestjs/common'; 2 | import { FastifyBaseLogger } from 'fastify'; 3 | 4 | @Injectable({ scope: Scope.TRANSIENT }) 5 | export class LoggerService implements LoggerBase { 6 | constructor(private readonly logger: FastifyBaseLogger) {} 7 | 8 | private formatMessage(message: any, context?: string): string { 9 | const formattedMessage = 10 | typeof message === 'object' ? JSON.stringify(message) : message; 11 | return context ? `${formattedMessage}` : formattedMessage; 12 | } 13 | 14 | log(message: any, context?: string) { 15 | const formattedMessage = this.formatMessage(message, context); 16 | this.logger.info({ context }, formattedMessage); 17 | } 18 | 19 | error(message: any, context?: string) { 20 | const formattedMessage = this.formatMessage(message, context); 21 | this.logger.error({ context }, formattedMessage); 22 | } 23 | 24 | warn(message: any, context?: string) { 25 | const formattedMessage = this.formatMessage(message, context); 26 | this.logger.warn({ context }, formattedMessage); 27 | } 28 | 29 | debug(message: any, context?: string) { 30 | const formattedMessage = this.formatMessage(message, context); 31 | this.logger.debug({ context }, formattedMessage); 32 | } 33 | 34 | verbose(message: any, context?: string) { 35 | const formattedMessage = this.formatMessage(message, context); 36 | this.logger.trace({ context }, formattedMessage); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/mailer/mailer.constants.ts: -------------------------------------------------------------------------------- 1 | import { ChangePasswordDto } from '@/iam/change-password/dto/change-password.dto'; 2 | import { RegisterUserDto } from '@/iam/register/dto/register-user.dto'; 3 | 4 | export const registrationEmail = (user: RegisterUserDto) => { 5 | return ` 6 | 7 |
8 |

Hi ${user.name}!

9 |

You did it! You registered!, You're successfully registered.✔

10 |
11 | 12 | 13 | `; 14 | }; 15 | 16 | export const forgotPasswordEmail = (password: string) => { 17 | return ` 18 | 19 |
20 |

Request Reset Password Successfully! ✔

21 |

This is your new password: ${password}

22 |
23 | 24 | 25 | `; 26 | }; 27 | 28 | export const changePasswordEmail = (user: ChangePasswordDto) => { 29 | return ` 30 | 31 |
32 |

Change Password Successfully! ✔

33 |

this is your new password: ${user.password}

34 |
35 | 36 | 37 | `; 38 | }; 39 | -------------------------------------------------------------------------------- /src/common/mailer/mailer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MailerService } from './mailer.service'; 3 | 4 | @Module({ 5 | providers: [MailerService], 6 | exports: [MailerService], 7 | }) 8 | export class MailerModule {} 9 | -------------------------------------------------------------------------------- /src/common/mailer/mailer.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MailerService } from './mailer.service'; 3 | 4 | describe('MailerService', () => { 5 | let service: MailerService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [MailerService], 10 | }).compile(); 11 | 12 | service = module.get(MailerService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/common/mailer/mailer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { createTransport, SendMailOptions } from 'nodemailer'; 3 | import * as Mail from 'nodemailer/lib/mailer'; 4 | 5 | @Injectable() 6 | export class MailerService { 7 | private nodemailerTransport: Mail; 8 | 9 | constructor() { 10 | this.nodemailerTransport = createTransport({ 11 | host: process.env.EMAIL_HOST, 12 | port: process.env.EMAIL_PORT ? parseInt(process.env.EMAIL_PORT, 10) : 587, // default 587 13 | auth: { 14 | user: process.env.EMAIL_AUTH_USER, 15 | pass: process.env.EMAIL_AUTH_PASSWORD, 16 | }, 17 | debug: process.env.EMAIL_DEBUG === 'true', 18 | logger: false, 19 | }); 20 | } 21 | 22 | sendMail(options: SendMailOptions) { 23 | return this.nodemailerTransport.sendMail(options); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/plugins/register-fastify.plugins.ts: -------------------------------------------------------------------------------- 1 | import { NestFastifyApplication } from '@nestjs/platform-fastify'; 2 | 3 | export async function registerFastifyPlugins(app: NestFastifyApplication) { 4 | 5 | await app.register(require('@fastify/cors'), { 6 | origin: true || [process.env.ENDPOINT_URL_CORS], 7 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 8 | allowedHeaders: 9 | 'Content-Type, Accept, Access-Control-Allow-Origin, Access-Control-Allow-Methods', 10 | credentials: true, 11 | }); 12 | 13 | await app.register(require('@fastify/rate-limit'), { 14 | max: 100, 15 | timeWindow: '1 minute', 16 | }); 17 | 18 | await app.register(require('@fastify/helmet'), { 19 | crossOriginResourcePolicy: true, 20 | contentSecurityPolicy: false, 21 | referrerPolicy: { 22 | policy: 'same-origin', 23 | }, 24 | hsts: { 25 | maxAge: 31536000, // 1 year 26 | includeSubDomains: true, // Optional: Include subdomains 27 | preload: true, // Optional: Indicate to browsers to preload HSTS 28 | }, 29 | frameguard: { 30 | action: 'deny', 31 | }, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/common/utils/utils.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UtilsService } from './utils.service'; 3 | 4 | @Module({ 5 | providers: [UtilsService], 6 | exports: [UtilsService], 7 | }) 8 | export class UtilsModule {} 9 | -------------------------------------------------------------------------------- /src/common/utils/utils.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UtilsService } from './utils.service'; 3 | 4 | describe('UtilsService', () => { 5 | let service: UtilsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UtilsService], 10 | }).compile(); 11 | 12 | service = module.get(UtilsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/common/utils/utils.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as crypto from 'crypto'; 3 | 4 | @Injectable() 5 | export class UtilsService { 6 | public generatePassword(): string { 7 | return crypto.randomUUID(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum DataSource { 2 | TYPEORM = 'typeorm', 3 | } 4 | -------------------------------------------------------------------------------- /src/helpers/configure-auth-swagger-docs.helper.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | 3 | export function configureAuthSwaggerDocs(app: INestApplication) { 4 | const apiDocumentationCredentials = { 5 | user: process.env.SWAGGER_USER, 6 | password: process.env.SWAGGER_PASSWORD, 7 | }; 8 | 9 | const httpAdapter = app.getHttpAdapter(); 10 | httpAdapter.use('/docs', (req: any, res: any, next: () => void) => { 11 | function parseAuthHeader(input: string): { 12 | user: string; 13 | password: string; 14 | } { 15 | const [, encodedPart] = input.split(' '); 16 | const buff = Buffer.from(encodedPart, 'base64'); 17 | const text = buff.toString('ascii'); 18 | const [user, password] = text.split(':'); 19 | return { user, password }; 20 | } 21 | 22 | function unauthorizedResponse(): void { 23 | if (httpAdapter.getType() === 'fastify') { 24 | res.statusCode = 401; 25 | res.setHeader('WWW-Authenticate', 'Basic'); 26 | } else { 27 | res.status(401); 28 | res.set('WWW-Authenticate', 'Basic'); 29 | } 30 | next(); 31 | } 32 | 33 | if (!req.headers.authorization) { 34 | return unauthorizedResponse(); 35 | } 36 | 37 | const credentials = parseAuthHeader(req.headers.authorization); 38 | 39 | if ( 40 | credentials?.user !== apiDocumentationCredentials.user || 41 | credentials?.password !== apiDocumentationCredentials.password 42 | ) { 43 | return unauthorizedResponse(); 44 | } 45 | 46 | next(); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/helpers/configure-swagger-docs.helper.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 3 | 4 | export function configureSwaggerDocs(app: INestApplication) { 5 | if (process.env.NODE_ENV !== 'production') { 6 | const config = new DocumentBuilder() 7 | .setTitle('API') 8 | .setDescription('The API description') 9 | .setVersion('1.0') 10 | .addServer('http://localhost:3000', 'Local server') 11 | .addTag('auth') 12 | .addTag('users') 13 | .addTag('app') 14 | .addBearerAuth({ 15 | description: 'Please enter token:', 16 | name: 'Authorization', 17 | bearerFormat: 'Bearer', 18 | scheme: 'Bearer', 19 | type: 'http', 20 | in: 'Header', 21 | }) 22 | .build(); 23 | const documentFactory = () => SwaggerModule.createDocument(app, config); 24 | SwaggerModule.setup('/docs', app, documentFactory, { 25 | explorer: true, 26 | swaggerOptions: { 27 | filter: true, 28 | showRequestDuration: true, 29 | }, 30 | jsonDocumentUrl: '/docs/json', 31 | yamlDocumentUrl: '/docs/yaml', 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/helpers/validation-schema-env.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | 3 | const ajv = new Ajv({ allErrors: true, useDefaults: true }); 4 | 5 | const schema = { 6 | type: 'object', 7 | properties: { 8 | TYPEORM_HOST: { type: 'string' }, 9 | TYPEORM_PORT: { type: 'string' }, 10 | TYPEORM_USERNAME: { type: 'string' }, 11 | TYPEORM_PASSWORD: { type: 'string' }, 12 | TYPEORM_DATABASE: { type: 'string' }, 13 | }, 14 | required: [ 15 | 'TYPEORM_HOST', 16 | 'TYPEORM_PORT', 17 | 'TYPEORM_USERNAME', 18 | 'TYPEORM_PASSWORD', 19 | 'TYPEORM_DATABASE', 20 | ], 21 | }; 22 | 23 | const validate = ajv.compile(schema); 24 | 25 | interface EnvVariables { 26 | TYPEORM_HOST: string; 27 | TYPEORM_PORT: string; 28 | TYPEORM_USERNAME: string; 29 | TYPEORM_PASSWORD: string; 30 | TYPEORM_DATABASE: string; 31 | } 32 | 33 | export const validateSchemaEnv = (env: unknown) => { 34 | const valid = validate(env); 35 | if (!valid) { 36 | const errorMessages = validate.errors 37 | ?.map( 38 | (err: { instancePath?: string; message?: string }) => 39 | `- ${err.instancePath || ''} ${err.message || 'Unknown error'}`, 40 | ) 41 | .join('\n') ?? 'Unknown error'; 42 | console.error(`Environment validation error: \n${errorMessages}`); 43 | } 44 | return env as EnvVariables; 45 | }; 46 | -------------------------------------------------------------------------------- /src/iam/change-password/change-password.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { ChangePasswordController } from './change-password.controller'; 4 | import { ChangePasswordService } from './change-password.service'; 5 | import { ChangePasswordDto } from './dto/change-password.dto'; 6 | 7 | const changePasswordDto: ChangePasswordDto = { 8 | email: 'text@example.com', 9 | password: 'password123', 10 | }; 11 | 12 | describe('ChangePassword Controller', () => { 13 | let changePasswordController: ChangePasswordController; 14 | let changePasswordService: ChangePasswordService; 15 | 16 | beforeEach(async () => { 17 | const module: TestingModule = await Test.createTestingModule({ 18 | controllers: [ChangePasswordController], 19 | providers: [ 20 | { 21 | provide: ChangePasswordService, 22 | useValue: { 23 | changePassword: jest.fn(() => {}), 24 | }, 25 | }, 26 | ], 27 | }).compile(); 28 | 29 | changePasswordController = module.get( 30 | ChangePasswordController, 31 | ); 32 | changePasswordService = module.get( 33 | ChangePasswordService, 34 | ); 35 | }); 36 | 37 | describe('Change Password', () => { 38 | it('should be defined', () => { 39 | expect(changePasswordController).toBeDefined(); 40 | }); 41 | 42 | it('should call method changePassword in changePasswordService', async () => { 43 | const createSpy = jest.spyOn(changePasswordService, 'changePassword'); 44 | 45 | await changePasswordController.changePassword(changePasswordDto); 46 | expect(createSpy).toHaveBeenCalledWith(changePasswordDto); 47 | }); 48 | 49 | it('should throw an exception if it not find an user email', async () => { 50 | changePasswordService.changePassword = jest 51 | .fn() 52 | .mockRejectedValueOnce(null); 53 | await expect( 54 | changePasswordController.changePassword({ 55 | email: 'not a correct email', 56 | password: 'not a correct password', 57 | }), 58 | ).rejects.toThrow(BadRequestException); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/iam/change-password/change-password.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | HttpStatus, 6 | BadRequestException, 7 | HttpCode, 8 | } from '@nestjs/common'; 9 | import { ChangePasswordService } from './change-password.service'; 10 | import { ChangePasswordDto } from './dto/change-password.dto'; 11 | import { 12 | ApiBadRequestResponse, 13 | ApiBearerAuth, 14 | ApiOkResponse, 15 | ApiTags, 16 | } from '@nestjs/swagger'; 17 | import { AuthGuard } from '../login/decorators/auth-guard.decorator'; 18 | import { AuthType } from '../login/enums/auth-type.enum'; 19 | 20 | interface ChangePasswordResponse { 21 | message: string; 22 | status: number; 23 | } 24 | 25 | @ApiTags('auth') 26 | @ApiBearerAuth() 27 | @AuthGuard(AuthType.Bearer) 28 | @Controller('auth/change-password') 29 | export class ChangePasswordController { 30 | constructor(private readonly changePasswordService: ChangePasswordService) {} 31 | 32 | @Post() 33 | @HttpCode(200) 34 | @ApiOkResponse({ 35 | description: 36 | 'Request Change Password and send a confirmation email to the user', 37 | }) 38 | @ApiBadRequestResponse({ description: 'Bad request' }) 39 | public async changePassword( 40 | @Body() changePasswordDto: ChangePasswordDto, 41 | ): Promise { 42 | try { 43 | await this.changePasswordService.changePassword(changePasswordDto); 44 | 45 | return { 46 | message: 'Request Change Password Successfully!', 47 | status: HttpStatus.OK, 48 | }; 49 | } catch (err) { 50 | throw new BadRequestException(err, 'Error: Change password failed!'); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/iam/change-password/change-password.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ChangePasswordController } from './change-password.controller'; 3 | import { ChangePasswordService } from './change-password.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Users } from '../../users/models/users.model'; 6 | import { UsersService } from '../../users/users.service'; 7 | import { MailerModule } from '../../common/mailer/mailer.module'; 8 | import { Argon2Service } from '../../common/hashing/argon2.service'; 9 | import { HashingService } from '../../common/hashing/hashing.service'; 10 | import { APP_GUARD } from '@nestjs/core'; 11 | import { AuthenticationGuard } from '../login/guards/authentication/authentication.guard'; 12 | import { AccessTokenGuard } from '../login/guards/access-token/access-token.guard'; 13 | import { JwtService } from '@nestjs/jwt'; 14 | import { provideUsersRepository } from '../../users/repositories/users.repository.provider'; 15 | 16 | @Module({ 17 | imports: [TypeOrmModule.forFeature([Users]), MailerModule], 18 | controllers: [ChangePasswordController], 19 | providers: [ 20 | { 21 | provide: HashingService, 22 | useClass: Argon2Service, 23 | }, 24 | { 25 | provide: APP_GUARD, 26 | useClass: AuthenticationGuard, 27 | }, 28 | AccessTokenGuard, 29 | ChangePasswordService, 30 | UsersService, 31 | JwtService, 32 | ...provideUsersRepository(), 33 | ], 34 | }) 35 | export class ChangePasswordModule {} 36 | -------------------------------------------------------------------------------- /src/iam/change-password/change-password.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ChangePasswordService } from './change-password.service'; 3 | import { getRepositoryToken } from '@nestjs/typeorm'; 4 | import { Users } from '../../users/models/users.model'; 5 | import { Repository } from 'typeorm'; 6 | import { UsersService } from '../../users/users.service'; 7 | import { MailerService } from '../../common/mailer/mailer.service'; 8 | 9 | const changePasswordUser = { 10 | email: 'test@example.it', 11 | password: '1234567', 12 | }; 13 | 14 | describe('ChangePasswordService', () => { 15 | let service: ChangePasswordService; 16 | let repository: Repository; 17 | 18 | beforeEach(async () => { 19 | const module: TestingModule = await Test.createTestingModule({ 20 | providers: [ 21 | ChangePasswordService, 22 | { 23 | provide: UsersService, 24 | useValue: { 25 | updateByPassword: jest.fn().mockResolvedValue(changePasswordUser), 26 | }, 27 | }, 28 | { 29 | provide: MailerService, 30 | useValue: { 31 | sendMail: jest.fn(), 32 | }, 33 | }, 34 | { 35 | provide: getRepositoryToken(Users), 36 | useValue: { 37 | findOneBy: jest.fn(), 38 | updateByPassword: jest.fn(), 39 | save: jest.fn(), 40 | }, 41 | }, 42 | ], 43 | }).compile(); 44 | 45 | service = module.get(ChangePasswordService); 46 | repository = module.get>(getRepositoryToken(Users)); 47 | }); 48 | 49 | describe('change password user', () => { 50 | it('should be defined', () => { 51 | expect(service).toBeDefined(); 52 | }); 53 | 54 | it('should change password a user', () => { 55 | expect( 56 | service.changePassword({ 57 | email: 'test@example.it', 58 | password: '1234567', 59 | }), 60 | ).resolves.toEqual(changePasswordUser); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/iam/change-password/change-password.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { UsersService } from '../../users/users.service'; 3 | import { ChangePasswordDto } from './dto/change-password.dto'; 4 | import { MailerService } from '../../common/mailer/mailer.service'; 5 | import { changePasswordEmail } from '../../common/mailer/mailer.constants'; 6 | import { Users } from '@/users/models/users.model'; 7 | 8 | @Injectable() 9 | export class ChangePasswordService { 10 | constructor( 11 | private readonly usersService: UsersService, 12 | private readonly mailerService: MailerService, 13 | ) {} 14 | 15 | public async changePassword( 16 | changePasswordDto: ChangePasswordDto, 17 | ): Promise { 18 | this.sendMailChangePassword(changePasswordDto).catch((err: unknown) => 19 | Logger.error('Change Password: Send Mail Failed!', err), 20 | ); 21 | 22 | return await this.usersService.updateByPassword( 23 | changePasswordDto.email, 24 | changePasswordDto.password, 25 | ); 26 | } 27 | 28 | private async sendMailChangePassword(user: ChangePasswordDto): Promise { 29 | try { 30 | await this.mailerService.sendMail({ 31 | to: user.email, 32 | from: 'from@example.com', 33 | subject: 'Change Password successful ✔', 34 | text: 'Change Password successful!', 35 | html: changePasswordEmail(user), 36 | }); 37 | Logger.log('Change Password: Send Mail successfully!', 'MailService'); 38 | } catch (err: unknown) { 39 | Logger.error('Change Password: Send Mail Failed!', err); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/iam/change-password/dto/change-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { UserDto } from '../../../users/dto/user.dto'; 3 | 4 | export class ChangePasswordDto extends PickType(UserDto, [ 5 | 'email', 6 | 'password', 7 | ] as const) {} 8 | -------------------------------------------------------------------------------- /src/iam/forgot-password/dto/forgot-password.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { UserDto } from '../../../users/dto/user.dto'; 3 | 4 | export class ForgotPasswordDto extends PickType(UserDto, ['email'] as const) {} 5 | -------------------------------------------------------------------------------- /src/iam/forgot-password/forgot-password.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ForgotPasswordController } from './forgot-password.controller'; 3 | import { ForgotPasswordService } from './forgot-password.service'; 4 | import { ForgotPasswordDto } from './dto/forgot-password.dto'; 5 | import { BadRequestException } from '@nestjs/common'; 6 | 7 | const forgotPasswordDto: ForgotPasswordDto = { 8 | email: 'test@example.com', 9 | }; 10 | 11 | describe('ForgotPassword Controller', () => { 12 | let forgotPasswordController: ForgotPasswordController; 13 | let forgotPasswordService: ForgotPasswordService; 14 | 15 | beforeEach(async () => { 16 | const module: TestingModule = await Test.createTestingModule({ 17 | controllers: [ForgotPasswordController], 18 | providers: [ 19 | { 20 | provide: ForgotPasswordService, 21 | useValue: { 22 | forgotPassword: jest.fn(() => {}), 23 | }, 24 | }, 25 | ], 26 | }).compile(); 27 | 28 | forgotPasswordController = module.get( 29 | ForgotPasswordController, 30 | ); 31 | forgotPasswordService = module.get( 32 | ForgotPasswordService, 33 | ); 34 | }); 35 | 36 | describe('Forgot Password', () => { 37 | it('should be defined', () => { 38 | expect(forgotPasswordController).toBeDefined(); 39 | }); 40 | 41 | it('should call method forgotPassword in forgotPasswordService', async () => { 42 | const createSpy = jest.spyOn(forgotPasswordService, 'forgotPassword'); 43 | 44 | await forgotPasswordController.forgotPassword(forgotPasswordDto); 45 | expect(createSpy).toHaveBeenCalledWith(forgotPasswordDto); 46 | }); 47 | 48 | it('should throw an exception if it not find an user email', async () => { 49 | forgotPasswordService.forgotPassword = jest 50 | .fn() 51 | .mockRejectedValueOnce(null); 52 | await expect( 53 | forgotPasswordController.forgotPassword({ 54 | email: 'not a correct email', 55 | }), 56 | ).rejects.toThrow(BadRequestException); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/iam/forgot-password/forgot-password.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | HttpStatus, 6 | BadRequestException, 7 | HttpCode, 8 | } from '@nestjs/common'; 9 | import { 10 | ApiBadRequestResponse, 11 | ApiBearerAuth, 12 | ApiOkResponse, 13 | ApiTags, 14 | } from '@nestjs/swagger'; 15 | import { ForgotPasswordService } from '../forgot-password/forgot-password.service'; 16 | import { AuthGuard } from '../login/decorators/auth-guard.decorator'; 17 | import { AuthType } from '../login/enums/auth-type.enum'; 18 | import { ForgotPasswordDto } from './dto/forgot-password.dto'; 19 | 20 | interface ForgotPasswordResponse { 21 | message: string; 22 | status: number; 23 | } 24 | 25 | @ApiTags('auth') 26 | @ApiBearerAuth() 27 | @AuthGuard(AuthType.None) 28 | @Controller('auth/forgot-password') 29 | export class ForgotPasswordController { 30 | constructor(private readonly forgotPasswordService: ForgotPasswordService) {} 31 | 32 | @Post() 33 | @HttpCode(200) 34 | @ApiOkResponse({ 35 | description: 36 | 'Request Reset Password and send a confirmation email to the user', 37 | }) 38 | @ApiBadRequestResponse({ description: 'Bad request' }) 39 | public async forgotPassword( 40 | @Body() forgotPasswordDto: ForgotPasswordDto, 41 | ): Promise { 42 | try { 43 | await this.forgotPasswordService.forgotPassword(forgotPasswordDto); 44 | 45 | return { 46 | message: 'Request Reset Password Successfully!', 47 | status: HttpStatus.OK, 48 | }; 49 | } catch (err) { 50 | throw new BadRequestException(err, 'Error: Forgot password failed!'); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/iam/forgot-password/forgot-password.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ForgotPasswordService } from './forgot-password.service'; 3 | import { ForgotPasswordController } from './forgot-password.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Argon2Service } from '../../common/hashing/argon2.service'; 6 | import { HashingService } from '../../common/hashing/hashing.service'; 7 | import { MailerModule } from '../../common/mailer/mailer.module'; 8 | import { UtilsModule } from '../../common/utils/utils.module'; 9 | import { Users } from '../../users/models/users.model'; 10 | import { provideUsersRepository } from '../../users/repositories/users.repository.provider'; 11 | import { UsersService } from '../../users/users.service'; 12 | 13 | @Module({ 14 | imports: [TypeOrmModule.forFeature([Users]), MailerModule, UtilsModule], 15 | providers: [ 16 | { 17 | provide: HashingService, 18 | useClass: Argon2Service, 19 | }, 20 | ForgotPasswordService, 21 | UsersService, 22 | ...provideUsersRepository(), 23 | ], 24 | controllers: [ForgotPasswordController], 25 | }) 26 | export class ForgotPasswordModule {} 27 | -------------------------------------------------------------------------------- /src/iam/forgot-password/forgot-password.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { getRepositoryToken } from '@nestjs/typeorm'; 3 | import { Users } from '../../users/models/users.model'; 4 | import { ForgotPasswordService } from './forgot-password.service'; 5 | import { MailerService } from '../../common/mailer/mailer.service'; 6 | import { UtilsService } from '../../common/utils/utils.service'; 7 | import { HashingService } from '../../common/hashing/hashing.service'; 8 | import { Repository } from 'typeorm'; 9 | import { UsersService } from '../../users/users.service'; 10 | 11 | const oneUser = { 12 | email: 'test@example.com', 13 | }; 14 | 15 | const user = { 16 | email: 'test@example.com', 17 | password: 'pass123', 18 | }; 19 | 20 | describe('ForgotPasswordService', () => { 21 | let service: ForgotPasswordService; 22 | let repository: Repository; 23 | let mailerService: MailerService; 24 | 25 | beforeEach(async () => { 26 | const module: TestingModule = await Test.createTestingModule({ 27 | providers: [ 28 | ForgotPasswordService, 29 | { 30 | provide: UsersService, 31 | useValue: { 32 | forgotPassword: jest.fn(), 33 | }, 34 | }, 35 | { 36 | provide: getRepositoryToken(Users), 37 | useValue: { 38 | findOneBy: jest.fn(() => oneUser), 39 | save: jest.fn(() => user), 40 | }, 41 | }, 42 | { 43 | provide: HashingService, 44 | useValue: { 45 | hash: jest.fn(() => 'pass123'), 46 | }, 47 | }, 48 | { 49 | provide: MailerService, 50 | useValue: { 51 | sendMail: jest.fn(), 52 | }, 53 | }, 54 | UtilsService, 55 | ], 56 | }).compile(); 57 | 58 | service = module.get(ForgotPasswordService); 59 | mailerService = module.get(MailerService); 60 | repository = module.get>(getRepositoryToken(Users)); 61 | }); 62 | 63 | describe('forgot password user', () => { 64 | it('should be defined', () => { 65 | expect(service).toBeDefined(); 66 | }); 67 | 68 | it('should generate a new password for user by email', async () => { 69 | expect( 70 | await service.forgotPassword({ 71 | email: 'test@example.com', 72 | }), 73 | ).toEqual(oneUser); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/iam/forgot-password/forgot-password.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { Repository } from 'typeorm'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Users } from '../../users/models/users.model'; 5 | import { ForgotPasswordDto } from './dto/forgot-password.dto'; 6 | import { MailerService } from '../../common/mailer/mailer.service'; 7 | import { UtilsService } from '../../common/utils/utils.service'; 8 | import { HashingService } from '../../common/hashing/hashing.service'; 9 | import { forgotPasswordEmail } from '../../common/mailer/mailer.constants'; 10 | 11 | @Injectable() 12 | export class ForgotPasswordService { 13 | constructor( 14 | @InjectRepository(Users) 15 | private readonly userRepository: Repository, 16 | private readonly mailerService: MailerService, 17 | private readonly utilsService: UtilsService, 18 | private readonly hashingService: HashingService, 19 | ) {} 20 | 21 | public async forgotPassword( 22 | forgotPasswordDto: ForgotPasswordDto, 23 | ): Promise { 24 | const userUpdate = await this.userRepository.findOneBy({ 25 | email: forgotPasswordDto.email, 26 | }); 27 | const passwordRand = this.utilsService.generatePassword(); 28 | userUpdate.password = await this.hashingService.hash(passwordRand); 29 | 30 | this.sendMailForgotPassword(userUpdate.email, passwordRand).catch( 31 | (err: unknown) => 32 | Logger.error('Forgot Password: Send Mail Failed (non bloccante)', err), 33 | ); 34 | 35 | return await this.userRepository.save(userUpdate); 36 | } 37 | 38 | private async sendMailForgotPassword( 39 | email: string, 40 | password: string, 41 | ): Promise { 42 | try { 43 | await this.mailerService.sendMail({ 44 | to: email, 45 | from: 'from@example.com', 46 | subject: 'Forgot Password successful ✔', 47 | text: 'Forgot Password successful!', 48 | html: forgotPasswordEmail(password), 49 | }); 50 | Logger.log('Forgot Password: Send Mail successfully!', 'MailService'); 51 | } catch (err) { 52 | Logger.error('Forgot Password: Send Mail Failed!', err); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/iam/iam.constants.ts: -------------------------------------------------------------------------------- 1 | export const REQUEST_USER_KEY = 'user'; 2 | export const TYPE_TOKEN_BEARER = 'Bearer'; 3 | -------------------------------------------------------------------------------- /src/iam/iam.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { UtilsModule } from '../common/utils/utils.module'; 4 | import { UsersModule } from '../users/users.module'; 5 | import { ChangePasswordModule } from './change-password/change-password.module'; 6 | import { ForgotPasswordModule } from './forgot-password/forgot-password.module'; 7 | import { LoginModule } from './login/login.module'; 8 | import { RegisterModule } from './register/register.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | LoginModule, 13 | RegisterModule, 14 | UsersModule, 15 | ForgotPasswordModule, 16 | ChangePasswordModule, 17 | UtilsModule, 18 | ], 19 | providers: [JwtService], 20 | }) 21 | export class IamModule {} 22 | -------------------------------------------------------------------------------- /src/iam/login/config/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | 3 | config(); 4 | 5 | export interface JwtConfig { 6 | secret: string; 7 | audience?: string; 8 | issuer?: string; 9 | accessTokenTtl?: number; 10 | refreshTokenTtl?: number; 11 | } 12 | 13 | export const jwtConfig: JwtConfig = { 14 | secret: process.env.JWT_SECRET_KEY || 'default-secret', 15 | audience: process.env.JWT_TOKEN_AUDIENCE || 'default-audience', 16 | issuer: process.env.JWT_TOKEN_ISSUER || 'default-issuer', 17 | accessTokenTtl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL || '3600', 10), 18 | refreshTokenTtl: parseInt(process.env.JWT_REFRESH_TOKEN_TTL || '86400', 10), 19 | }; 20 | -------------------------------------------------------------------------------- /src/iam/login/decorators/auth-guard.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { AuthType } from '../enums/auth-type.enum'; 3 | 4 | export const AUTH_TYPE_KEY = 'authType'; 5 | 6 | export const AuthGuard = (...authTypes: AuthType[]) => 7 | SetMetadata(AUTH_TYPE_KEY, authTypes); 8 | -------------------------------------------------------------------------------- /src/iam/login/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { PickType } from '@nestjs/swagger'; 2 | import { UserDto } from '../../../users/dto/user.dto'; 3 | 4 | export class LoginDto extends PickType(UserDto, [ 5 | 'email', 6 | 'password', 7 | ] as const) {} 8 | -------------------------------------------------------------------------------- /src/iam/login/dto/refresh-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class RefreshTokenDto { 4 | @IsNotEmpty() 5 | refreshToken: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/iam/login/enums/auth-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AuthType { 2 | Bearer, 3 | None, 4 | } 5 | -------------------------------------------------------------------------------- /src/iam/login/guards/access-token/access-token.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | HttpStatus, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { JwtService } from '@nestjs/jwt'; 9 | import { FastifyRequest } from 'fastify'; 10 | import { REQUEST_USER_KEY, TYPE_TOKEN_BEARER } from '../../../iam.constants'; 11 | import { jwtConfig } from '../../config/jwt.config'; 12 | 13 | @Injectable() 14 | export class AccessTokenGuard implements CanActivate { 15 | constructor(private readonly jwtService: JwtService) {} 16 | 17 | async canActivate(context: ExecutionContext): Promise { 18 | const request = context.switchToHttp().getRequest(); 19 | const token = this.extractTokenFromHeader(request); 20 | if (!token) { 21 | throw new UnauthorizedException(); 22 | } 23 | 24 | try { 25 | const payload = await this.jwtService.verifyAsync(token, { 26 | secret: jwtConfig.secret, 27 | audience: jwtConfig.audience, 28 | issuer: jwtConfig.issuer, 29 | }); 30 | request[REQUEST_USER_KEY] = payload; 31 | } catch (err) { 32 | throw new UnauthorizedException(HttpStatus.UNAUTHORIZED, err); 33 | } 34 | return true; 35 | } 36 | 37 | private extractTokenFromHeader(request: FastifyRequest): string | undefined { 38 | const [type, token] = request.headers.authorization?.split(' ') ?? []; 39 | return type === TYPE_TOKEN_BEARER ? token : undefined; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/iam/login/guards/authentication/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { Reflector } from '@nestjs/core'; 8 | import { AuthType } from '../../enums/auth-type.enum'; 9 | import { AccessTokenGuard } from '../access-token/access-token.guard'; 10 | import { AUTH_TYPE_KEY } from '../../decorators/auth-guard.decorator'; 11 | 12 | @Injectable() 13 | export class AuthenticationGuard implements CanActivate { 14 | private static readonly defaultAuthType = AuthType.Bearer; 15 | private readonly authTypeGuardMap: Record< 16 | AuthType, 17 | CanActivate | CanActivate[] 18 | > = { 19 | [AuthType.Bearer]: this.accessTokenGuard, 20 | [AuthType.None]: { canActivate: () => true }, 21 | }; 22 | 23 | constructor( 24 | private readonly reflector: Reflector, 25 | private readonly accessTokenGuard: AccessTokenGuard, 26 | ) {} 27 | 28 | async canActivate(context: ExecutionContext): Promise { 29 | const authTypes = this.reflector.getAllAndOverride( 30 | AUTH_TYPE_KEY, 31 | [context.getHandler(), context.getClass()], 32 | ) ?? [AuthenticationGuard.defaultAuthType]; 33 | const guards = authTypes.map((type) => this.authTypeGuardMap[type]).flat(); 34 | let error = new UnauthorizedException(); 35 | 36 | for (const instance of guards) { 37 | const canActivate = await Promise.resolve( 38 | instance.canActivate(context), 39 | ).catch((err) => { 40 | error = err; 41 | }); 42 | 43 | if (canActivate) { 44 | return true; 45 | } 46 | } 47 | throw error; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/iam/login/interfaces/auth-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AuthResponse { 2 | accessToken: string; 3 | refreshToken: string; 4 | user: { 5 | id: number; 6 | name: string; 7 | email: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/iam/login/interfaces/jwt-payload.interface.ts: -------------------------------------------------------------------------------- 1 | export interface JWTPayload { 2 | id: number; 3 | email: string; 4 | name: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/iam/login/login.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersService } from '../../users/users.service'; 3 | import { LoginController } from './login.controller'; 4 | import { LoginService } from './login.service'; 5 | import { LoginDto } from './dto/login.dto'; 6 | import { RefreshTokenDto } from './dto/refresh-token.dto'; 7 | 8 | const loginDto: LoginDto = { 9 | email: 'test@example.com', 10 | password: 'password123', 11 | }; 12 | 13 | const refreshTokenDto: RefreshTokenDto = { 14 | refreshToken: 'token', 15 | }; 16 | 17 | describe('Login Controller', () => { 18 | let loginController: LoginController; 19 | let loginService: LoginService; 20 | 21 | beforeEach(async () => { 22 | const module: TestingModule = await Test.createTestingModule({ 23 | controllers: [LoginController], 24 | providers: [ 25 | { 26 | provide: LoginService, 27 | useValue: { 28 | login: jest.fn(() => {}), 29 | refreshTokens: jest.fn(() => {}), 30 | }, 31 | }, 32 | { 33 | provide: UsersService, 34 | useValue: { 35 | findByEmail: jest.fn(() => {}), 36 | }, 37 | }, 38 | ], 39 | }).compile(); 40 | 41 | loginController = module.get(LoginController); 42 | loginService = module.get(LoginService); 43 | }); 44 | 45 | describe('Login user', () => { 46 | it('should be defined', () => { 47 | expect(loginController).toBeDefined(); 48 | }); 49 | 50 | it('should call method login in loginService', async () => { 51 | const createSpy = jest.spyOn(loginService, 'login'); 52 | 53 | await loginController.login(loginDto); 54 | expect(createSpy).toHaveBeenCalledWith(loginDto); 55 | }); 56 | 57 | it('should call method refresh tokens in loginService', async () => { 58 | const createSpy = jest.spyOn(loginService, 'refreshTokens'); 59 | 60 | await loginController.refreshTokens(refreshTokenDto); 61 | expect(createSpy).toHaveBeenCalledWith(refreshTokenDto); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/iam/login/login.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, HttpCode } from '@nestjs/common'; 2 | import { LoginService } from './login.service'; 3 | import { LoginDto } from '../login/dto/login.dto'; 4 | import { 5 | ApiUnauthorizedResponse, 6 | ApiOkResponse, 7 | ApiTags, 8 | ApiBearerAuth, 9 | } from '@nestjs/swagger'; 10 | import { AuthType } from './enums/auth-type.enum'; 11 | import { AuthGuard } from './decorators/auth-guard.decorator'; 12 | import { RefreshTokenDto } from './dto/refresh-token.dto'; 13 | import { AuthResponse } from './interfaces/auth-response.interface'; 14 | 15 | @ApiTags('auth') 16 | @AuthGuard(AuthType.None) 17 | @Controller('auth') 18 | export class LoginController { 19 | constructor(private readonly loginService: LoginService) {} 20 | 21 | @Post('login') 22 | @HttpCode(200) 23 | @ApiOkResponse({ 24 | description: 25 | 'Authentication a user with email and password credentials and return token', 26 | }) 27 | @ApiUnauthorizedResponse({ description: 'Forbidden' }) 28 | public async login(@Body() loginDto: LoginDto): Promise { 29 | return await this.loginService.login(loginDto); 30 | } 31 | 32 | @Post('refresh-tokens') 33 | @HttpCode(200) 34 | @ApiBearerAuth() 35 | @ApiOkResponse({ 36 | description: 'Refresh tokens and return new tokens', 37 | }) 38 | @ApiUnauthorizedResponse({ description: 'Forbidden' }) 39 | public async refreshTokens( 40 | @Body() refreshTokenDto: RefreshTokenDto, 41 | ): Promise { 42 | return await this.loginService.refreshTokens(refreshTokenDto); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/iam/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LoginService } from './login.service'; 3 | import { LoginController } from './login.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Users } from '../../users/models/users.model'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { UsersService } from '../../users/users.service'; 8 | import { HashingService } from '../../common/hashing/hashing.service'; 9 | import { Argon2Service } from '../../common/hashing/argon2.service'; 10 | import { APP_GUARD } from '@nestjs/core'; 11 | import { AuthenticationGuard } from './guards/authentication/authentication.guard'; 12 | import { AccessTokenGuard } from './guards/access-token/access-token.guard'; 13 | import { jwtConfig } from './config/jwt.config'; 14 | import { provideUsersRepository } from '../../users/repositories/users.repository.provider'; 15 | 16 | @Module({ 17 | imports: [ 18 | TypeOrmModule.forFeature([Users]), 19 | JwtModule.registerAsync({ 20 | useFactory: () => { 21 | if (!jwtConfig.secret) { 22 | throw new Error('JWT_SECRET_KEY not defined'); 23 | } 24 | return { 25 | secret: jwtConfig.secret, 26 | audience: jwtConfig.audience, 27 | issuer: jwtConfig.issuer, 28 | expiresIn: jwtConfig.accessTokenTtl, 29 | }; 30 | }, 31 | }), 32 | ], 33 | providers: [ 34 | { 35 | provide: HashingService, 36 | useClass: Argon2Service, 37 | }, 38 | { 39 | provide: APP_GUARD, 40 | useClass: AuthenticationGuard, 41 | }, 42 | AccessTokenGuard, 43 | LoginService, 44 | UsersService, 45 | ...provideUsersRepository(), 46 | ], 47 | controllers: [LoginController], 48 | }) 49 | export class LoginModule {} 50 | -------------------------------------------------------------------------------- /src/iam/login/login.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtService } from '@nestjs/jwt'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { HashingService } from '../../common/hashing/hashing.service'; 4 | import { LoginService } from './login.service'; 5 | import { UsersService } from '../../users/users.service'; 6 | import { Users } from '../../users/models/users.model'; 7 | import { getRepositoryToken } from '@nestjs/typeorm'; 8 | import { LoginDto } from './dto/login.dto'; 9 | import { UnauthorizedException, HttpException } from '@nestjs/common'; 10 | 11 | const oneUser = { 12 | id: 1, 13 | name: 'name #1', 14 | username: 'username #1', 15 | email: 'test@example.com', 16 | password: 'pass123', 17 | }; 18 | 19 | const loginDto: LoginDto = { 20 | email: 'test@example.com', 21 | password: 'pass123', 22 | }; 23 | 24 | const userLogin = { 25 | accessToken: undefined as any, 26 | refreshToken: undefined as any, 27 | user: { 28 | id: 1, 29 | name: 'name #1', 30 | email: 'test@example.com', 31 | }, 32 | }; 33 | 34 | const payload = { 35 | id: 1, 36 | name: 'name #1', 37 | email: 'test@example.com', 38 | }; 39 | 40 | const refreshTokenDto = { 41 | refreshToken: 'token', 42 | }; 43 | 44 | const id = 1; 45 | 46 | const jwtConfig = { 47 | secret: 'test-secret', 48 | audience: 'test-audience', 49 | issuer: 'test-issuer', 50 | accessTokenTtl: 3600, 51 | refreshTokenTtl: 86400, 52 | }; 53 | 54 | describe('LoginService', () => { 55 | let loginService: LoginService; 56 | let usersService: UsersService; 57 | let hashingService: HashingService; 58 | let jwtService: JwtService; 59 | 60 | beforeEach(async () => { 61 | const module: TestingModule = await Test.createTestingModule({ 62 | providers: [ 63 | LoginService, 64 | { 65 | provide: JwtService, 66 | useValue: { 67 | signAsync: jest.fn(), 68 | signToken: jest.fn(() => payload), 69 | verifyAsync: jest.fn(), 70 | }, 71 | }, 72 | { 73 | provide: 'jwtConfig', 74 | useValue: jwtConfig, 75 | }, 76 | { 77 | provide: HashingService, 78 | useValue: { 79 | hash: jest.fn(() => Promise.resolve('pass123')), 80 | compare: jest.fn(() => Promise.resolve(true)), 81 | }, 82 | }, 83 | { 84 | provide: UsersService, 85 | useValue: { 86 | findByEmail: jest.fn().mockResolvedValue(oneUser), 87 | findBySub: jest.fn().mockResolvedValue(oneUser), 88 | }, 89 | }, 90 | { 91 | provide: getRepositoryToken(Users), 92 | useValue: { 93 | findByEmail: jest.fn(), 94 | findOneBy: jest.fn().mockReturnValue(oneUser), 95 | findOne: jest.fn().mockReturnValue(oneUser), 96 | findBySub: jest.fn().mockReturnValueOnce(oneUser), 97 | }, 98 | }, 99 | ], 100 | }).compile(); 101 | 102 | loginService = module.get(LoginService); 103 | usersService = module.get(UsersService); 104 | hashingService = module.get(HashingService); 105 | jwtService = module.get(JwtService); 106 | }); 107 | 108 | it('should be defined', () => { 109 | expect(loginService).toBeDefined(); 110 | }); 111 | 112 | describe('findUserByEmail() method', () => { 113 | it('should find a user by email', async () => { 114 | expect(await loginService.findUserByEmail(loginDto)).toEqual(oneUser); 115 | }); 116 | 117 | it('should generate token jwt', async () => { 118 | expect(await loginService.login(loginDto)).toEqual(userLogin); 119 | }); 120 | 121 | it('should generate refresh token jwt', async () => { 122 | usersService.findBySub = jest.fn().mockResolvedValueOnce(oneUser); 123 | jwtService.verifyAsync = jest.fn(() => id as any); 124 | 125 | expect( 126 | await loginService.refreshTokens({ 127 | refreshToken: 'token', 128 | }), 129 | ).toEqual(userLogin); 130 | }); 131 | 132 | it('should return an exception if refresh token fails', async () => { 133 | usersService.findBySub = jest.fn().mockResolvedValueOnce(null); 134 | await expect( 135 | loginService.refreshTokens({ 136 | refreshToken: 'not a correct token jwt', 137 | }), 138 | ).rejects.toThrow(UnauthorizedException); 139 | }); 140 | 141 | it('should return an exception if wrong password', async () => { 142 | usersService.findByEmail = jest.fn().mockResolvedValueOnce(oneUser); 143 | hashingService.compare = jest.fn().mockResolvedValueOnce(false); 144 | await expect( 145 | loginService.login({ 146 | email: 'someemail@test.com', 147 | password: 'not a correct password', 148 | }), 149 | ).rejects.toThrow(HttpException); 150 | }); 151 | 152 | it('should return an exception if login fails', async () => { 153 | usersService.findByEmail = jest.fn().mockResolvedValueOnce(null); 154 | await expect( 155 | loginService.login({ 156 | email: 'not a correct email', 157 | password: 'not a correct password', 158 | }), 159 | ).rejects.toThrow(HttpException); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/iam/login/login.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpException, 3 | HttpStatus, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { UsersService } from '../../users/users.service'; 9 | import { AccountsUsers } from '../../users/interfaces/accounts-users.interface'; 10 | import { LoginDto } from './dto/login.dto'; 11 | import { HashingService } from '../../common/hashing/hashing.service'; 12 | import { JWTPayload } from './interfaces/jwt-payload.interface'; 13 | import { RefreshTokenDto } from './dto/refresh-token.dto'; 14 | import { Users } from '../../users/models/users.model'; 15 | import { jwtConfig } from './config/jwt.config'; 16 | import { AuthResponse } from './interfaces/auth-response.interface'; 17 | 18 | @Injectable() 19 | export class LoginService { 20 | constructor( 21 | private readonly usersService: UsersService, 22 | private readonly jwtService: JwtService, 23 | private readonly hashingService: HashingService, 24 | ) {} 25 | 26 | public async findUserByEmail(loginDto: LoginDto): Promise { 27 | return await this.usersService.findByEmail(loginDto.email); 28 | } 29 | 30 | public async login(loginDto: LoginDto): Promise { 31 | try { 32 | const user = await this.findUserByEmail(loginDto); 33 | if (!user) { 34 | throw new UnauthorizedException('User does not exists'); 35 | } 36 | 37 | const passwordIsValid = await this.hashingService.compare( 38 | loginDto.password, 39 | user.password, 40 | ); 41 | 42 | if (!passwordIsValid) { 43 | throw new UnauthorizedException( 44 | 'Authentication failed. Wrong password', 45 | ); 46 | } 47 | 48 | return await this.generateTokens(user); 49 | } catch (err) { 50 | throw new HttpException(err, HttpStatus.BAD_REQUEST); 51 | } 52 | } 53 | 54 | public async generateTokens(user: Users): Promise { 55 | const [accessToken, refreshToken] = await Promise.all([ 56 | this.signToken>( 57 | user.id, 58 | jwtConfig.accessTokenTtl ?? 3600, 59 | { email: user.email }, 60 | ), 61 | this.signToken(user.id, jwtConfig.refreshTokenTtl ?? 86400), 62 | ]); 63 | return { 64 | accessToken, 65 | refreshToken, 66 | user: { 67 | id: user.id, 68 | name: user.name, 69 | email: user.email, 70 | }, 71 | }; 72 | } 73 | 74 | public async refreshTokens( 75 | refreshTokenDto: RefreshTokenDto, 76 | ): Promise { 77 | try { 78 | const { id } = await this.jwtService.verifyAsync>( 79 | refreshTokenDto.refreshToken, 80 | { 81 | secret: jwtConfig.secret, 82 | audience: jwtConfig.audience, 83 | issuer: jwtConfig.issuer, 84 | }, 85 | ); 86 | const user = await this.usersService.findBySub(id); 87 | return this.generateTokens(user); 88 | } catch (err) { 89 | throw new UnauthorizedException(err); 90 | } 91 | } 92 | 93 | private async signToken( 94 | userId: number, 95 | expiresIn: number, 96 | payload?: T, 97 | ): Promise { 98 | return await this.jwtService.signAsync( 99 | { 100 | sub: userId, 101 | ...payload, 102 | }, 103 | { 104 | audience: jwtConfig.audience, 105 | issuer: jwtConfig.issuer, 106 | secret: jwtConfig.secret, 107 | expiresIn, 108 | }, 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/iam/register/dto/register-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { UserDto } from '../../../users/dto/user.dto'; 2 | 3 | export class RegisterUserDto extends UserDto {} 4 | -------------------------------------------------------------------------------- /src/iam/register/register.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { RegisterController } from './register.controller'; 3 | import { RegisterService } from './register.service'; 4 | import { UsersService } from '../../users/users.service'; 5 | import { MailerService } from '../../common/mailer/mailer.service'; 6 | import { RegisterUserDto } from './dto/register-user.dto'; 7 | import { BadRequestException } from '@nestjs/common'; 8 | 9 | const registerUserDto: RegisterUserDto = { 10 | name: 'name #1', 11 | username: 'username #1', 12 | email: 'test@example.com', 13 | password: 'password123', 14 | }; 15 | 16 | describe('Register Controller', () => { 17 | let registerController: RegisterController; 18 | let registerService: RegisterService; 19 | 20 | beforeEach(async () => { 21 | const module: TestingModule = await Test.createTestingModule({ 22 | controllers: [RegisterController], 23 | providers: [ 24 | RegisterService, 25 | { 26 | provide: MailerService, 27 | useValue: { 28 | sendMail: jest.fn(), 29 | }, 30 | }, 31 | { 32 | provide: UsersService, 33 | useValue: { 34 | register: jest.fn(() => {}), 35 | }, 36 | }, 37 | { 38 | provide: RegisterService, 39 | useValue: { 40 | register: jest.fn(() => {}), 41 | }, 42 | }, 43 | ], 44 | }).compile(); 45 | 46 | registerController = module.get(RegisterController); 47 | registerService = module.get(RegisterService); 48 | }); 49 | 50 | describe('Registration user', () => { 51 | it('should be defined', () => { 52 | expect(registerController).toBeDefined(); 53 | }); 54 | 55 | it('should call method register in registerService', async () => { 56 | const createSpy = jest.spyOn(registerService, 'register'); 57 | 58 | await registerController.register(registerUserDto); 59 | expect(createSpy).toHaveBeenCalledWith(registerUserDto); 60 | }); 61 | 62 | it('should throw an exception if it not register fails', async () => { 63 | registerService.register = jest.fn().mockRejectedValueOnce(null); 64 | await expect( 65 | registerController.register({ 66 | name: 'not a correct name', 67 | email: 'not a correct email', 68 | username: 'not a correct username', 69 | password: 'not a correct password', 70 | }), 71 | ).rejects.toThrow(BadRequestException); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/iam/register/register.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | HttpStatus, 6 | BadRequestException, 7 | HttpCode, 8 | } from '@nestjs/common'; 9 | import { RegisterService } from './register.service'; 10 | import { RegisterUserDto } from './dto/register-user.dto'; 11 | import { ApiBadRequestResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; 12 | import { AuthType } from '../login/enums/auth-type.enum'; 13 | import { AuthGuard } from '../login/decorators/auth-guard.decorator'; 14 | 15 | interface RegisterResponse { 16 | message: string; 17 | status: number; 18 | } 19 | @ApiTags('auth') 20 | @AuthGuard(AuthType.None) 21 | @Controller('auth/register') 22 | export class RegisterController { 23 | constructor(private readonly registerService: RegisterService) {} 24 | 25 | @Post() 26 | @HttpCode(201) 27 | @ApiOkResponse({ 28 | description: 29 | 'Register a new user and send a confirmation email to the user', 30 | }) 31 | @ApiBadRequestResponse({ description: 'Bad request' }) 32 | public async register( 33 | @Body() registerUserDto: RegisterUserDto, 34 | ): Promise { 35 | try { 36 | await this.registerService.register(registerUserDto); 37 | 38 | return { 39 | message: 'User registration successfully!', 40 | status: HttpStatus.CREATED, 41 | }; 42 | } catch (err) { 43 | throw new BadRequestException(err, 'Error: User not registration!'); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/iam/register/register.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Argon2Service } from '../../common/hashing/argon2.service'; 4 | import { HashingService } from '../../common/hashing/hashing.service'; 5 | import { MailerModule } from '../../common/mailer/mailer.module'; 6 | import { Users } from '../../users/models/users.model'; 7 | import { UsersService } from '../../users/users.service'; 8 | import { RegisterController } from './register.controller'; 9 | import { RegisterService } from './register.service'; 10 | import { provideUsersRepository } from '../../users/repositories/users.repository.provider'; 11 | 12 | @Module({ 13 | imports: [TypeOrmModule.forFeature([Users]), MailerModule], 14 | controllers: [RegisterController], 15 | providers: [ 16 | { 17 | provide: HashingService, 18 | useClass: Argon2Service, 19 | }, 20 | RegisterService, 21 | UsersService, 22 | ...provideUsersRepository(), 23 | ], 24 | }) 25 | export class RegisterModule {} 26 | -------------------------------------------------------------------------------- /src/iam/register/register.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersService } from '../../users/users.service'; 3 | import { RegisterService } from './register.service'; 4 | import { Users } from '../../users/models/users.model'; 5 | import { getRepositoryToken } from '@nestjs/typeorm'; 6 | import { RegisterUserDto } from './dto/register-user.dto'; 7 | import { HashingService } from '../../common/hashing/hashing.service'; 8 | import { MailerService } from '../../common/mailer/mailer.service'; 9 | import { Repository } from 'typeorm'; 10 | 11 | const registerUserDto: RegisterUserDto = { 12 | name: 'name #1', 13 | username: 'username #1', 14 | email: 'test@example.com', 15 | password: 'password123', 16 | }; 17 | 18 | describe('RegisterService', () => { 19 | let service: RegisterService; 20 | let repository: Repository; 21 | 22 | beforeEach(async () => { 23 | const module: TestingModule = await Test.createTestingModule({ 24 | providers: [ 25 | RegisterService, 26 | { 27 | provide: UsersService, 28 | useValue: { 29 | create: jest.fn().mockResolvedValue(registerUserDto), 30 | }, 31 | }, 32 | { 33 | provide: MailerService, 34 | useValue: { 35 | sendMail: jest.fn(), 36 | }, 37 | }, 38 | { 39 | provide: HashingService, 40 | useValue: { 41 | hash: jest.fn(), 42 | }, 43 | }, 44 | { 45 | provide: getRepositoryToken(Users), 46 | useValue: { 47 | save: jest.fn(), 48 | }, 49 | }, 50 | ], 51 | }).compile(); 52 | 53 | service = module.get(RegisterService); 54 | repository = module.get>(getRepositoryToken(Users)); 55 | }); 56 | 57 | describe('Create user', () => { 58 | it('should be defined', () => { 59 | expect(service).toBeDefined(); 60 | }); 61 | 62 | it('should create a user during registration', async () => { 63 | expect( 64 | await service.register({ 65 | name: 'name #1', 66 | username: 'username #1', 67 | email: 'test@example.com', 68 | password: 'password123', 69 | }), 70 | ).toEqual(registerUserDto); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/iam/register/register.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { HashingService } from '../../common/hashing/hashing.service'; 3 | import { MailerService } from '../../common/mailer/mailer.service'; 4 | import { AccountsUsers } from '../../users/interfaces/accounts-users.interface'; 5 | import { UsersService } from '../../users/users.service'; 6 | import { RegisterUserDto } from './dto/register-user.dto'; 7 | import { registrationEmail } from '../../common/mailer/mailer.constants'; 8 | 9 | @Injectable() 10 | export class RegisterService { 11 | constructor( 12 | private readonly usersService: UsersService, 13 | private readonly mailerService: MailerService, 14 | private readonly hashingService: HashingService, 15 | ) {} 16 | 17 | public async register( 18 | registerUserDto: RegisterUserDto, 19 | ): Promise { 20 | registerUserDto.password = await this.hashingService.hash( 21 | registerUserDto.password, 22 | ); 23 | 24 | this.sendMailRegisterUser(registerUserDto).catch((err: unknown) => 25 | Logger.error('Send mail failed but continuing registration', err), 26 | ); 27 | 28 | return this.usersService.create(registerUserDto); 29 | } 30 | 31 | private async sendMailRegisterUser(user: RegisterUserDto): Promise { 32 | try { 33 | await this.mailerService.sendMail({ 34 | to: user.email, 35 | from: 'from@example.com', 36 | subject: 'Registration successful ✔', 37 | html: registrationEmail(user), 38 | }); 39 | Logger.log('User Registration: Send Mail successfully!', 'MailService'); 40 | } catch (err: unknown) { 41 | Logger.error('User Registration: Send Mail failed!', err); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ConsoleLogger, Logger, ValidationPipe } from '@nestjs/common'; 4 | import { configureSwaggerDocs } from './helpers/configure-swagger-docs.helper'; 5 | import { configureAuthSwaggerDocs } from './helpers/configure-auth-swagger-docs.helper'; 6 | import { 7 | FastifyAdapter, 8 | NestFastifyApplication, 9 | } from '@nestjs/platform-fastify'; 10 | import { registerFastifyPlugins } from './common/plugins/register-fastify.plugins'; 11 | import { validateSchemaEnv } from './helpers/validation-schema-env'; 12 | import { config } from 'dotenv'; 13 | 14 | config(); 15 | 16 | validateSchemaEnv(process.env); 17 | 18 | async function bootstrap() { 19 | const fastifyAdapter = new FastifyAdapter(); 20 | const app = await NestFactory.create( 21 | AppModule, 22 | fastifyAdapter, 23 | { 24 | logger: new ConsoleLogger({ 25 | json: true, 26 | colors: true, 27 | }), 28 | }, 29 | ); 30 | 31 | // Plugins for Fastify 32 | registerFastifyPlugins(app); 33 | // Swagger Configurations 34 | configureAuthSwaggerDocs(app); 35 | configureSwaggerDocs(app); 36 | 37 | app.setGlobalPrefix('api'); 38 | app.useGlobalPipes( 39 | new ValidationPipe({ 40 | whitelist: true, 41 | transform: true, 42 | forbidNonWhitelisted: true, 43 | transformOptions: { 44 | enableImplicitConversion: true, 45 | }, 46 | }), 47 | ); 48 | 49 | const port = process.env.SERVER_PORT || 3000; 50 | await app.listen(port, '0.0.0.0'); 51 | if (process.env.NODE_ENV !== 'production') { 52 | Logger.debug( 53 | `${await app.getUrl()} - Environment: ${process.env.NODE_ENV}`, 54 | 'Environment', 55 | ); 56 | 57 | Logger.debug(`Url for OpenApi: ${await app.getUrl()}/docs`, 'Swagger'); 58 | } 59 | } 60 | bootstrap(); 61 | -------------------------------------------------------------------------------- /src/migrations/1589834500772-Api.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm"; 2 | 3 | export class Api1589834500772 implements MigrationInterface { 4 | name = 'Api1589834500772' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query("CREATE TABLE `user` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `username` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `password` varchar(60) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB", undefined); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query("DROP TABLE `user`", undefined); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/repl.ts: -------------------------------------------------------------------------------- 1 | import { repl } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | await repl(AppModule); 6 | } 7 | bootstrap(); 8 | -------------------------------------------------------------------------------- /src/users/dto/user-profile.dto.ts: -------------------------------------------------------------------------------- 1 | import { OmitType } from '@nestjs/swagger'; 2 | import { UserDto } from './user.dto'; 3 | 4 | export class UserProfileDto extends OmitType(UserDto, ['password'] as const) {} 5 | -------------------------------------------------------------------------------- /src/users/dto/user-update.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { UserDto } from './user.dto'; 3 | 4 | export class UserUpdateDto extends PartialType(UserDto) {} 5 | -------------------------------------------------------------------------------- /src/users/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { MaxLength, IsNotEmpty, IsEmail, IsString } from 'class-validator'; 2 | 3 | export class UserDto { 4 | @IsString() 5 | @MaxLength(30) 6 | readonly name: string; 7 | 8 | @IsString() 9 | @MaxLength(40) 10 | readonly username: string; 11 | 12 | @IsEmail() 13 | @IsString() 14 | @IsNotEmpty() 15 | readonly email: string; 16 | 17 | @IsNotEmpty() 18 | @IsString() 19 | @MaxLength(60) 20 | password: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/users/entities/user.entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user.entity'; 2 | 3 | describe('User class', () => { 4 | it('should make a user with no fields', () => { 5 | const user = new User('', '', '', '', ''); 6 | expect(user).toBeTruthy(); 7 | expect(user.name).toBe(''); 8 | expect(user.email).toBe(''); 9 | expect(user.username).toBe(''); 10 | expect(user.password).toBe(''); 11 | }); 12 | it('should make a user with fields', () => { 13 | const user = new User( 14 | '1', 15 | 'name#1', 16 | 'test@example.com', 17 | 'username#1', 18 | 'password#1', 19 | ); 20 | expect(user).toBeTruthy(); 21 | expect(user.name).toBe('name#1'); 22 | expect(user.email).toBe('test@example.com'); 23 | expect(user.username).toBe('username#1'); 24 | expect(user.password).toBe('password#1'); 25 | }); 26 | it('should make a user with name only', () => { 27 | const user = new User('1', 'name#1', '', '', ''); 28 | expect(user).toBeTruthy(); 29 | expect(user.name).toBe('name#1'); 30 | expect(user.email).toBe(''); 31 | expect(user.username).toBe(''); 32 | expect(user.password).toBe(''); 33 | }); 34 | it('should make a user with name and email', () => { 35 | const user = new User('1', 'name#1', 'test@example.com', '', ''); 36 | expect(user).toBeTruthy(); 37 | expect(user.name).toBe('name#1'); 38 | expect(user.email).toBe('test@example.com'); 39 | expect(user.username).toBe(''); 40 | expect(user.password).toBe(''); 41 | }); 42 | 43 | it('should make a user with name, email and username', () => { 44 | const user = new User('1', 'name#1', 'test@example.com', 'username#1', ''); 45 | expect(user).toBeTruthy(); 46 | expect(user.name).toBe('name#1'); 47 | expect(user.email).toBe('test@example.com'); 48 | expect(user.username).toBe('username#1'); 49 | expect(user.password).toBe(''); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/users/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | constructor( 3 | public id: string, 4 | public name: string, 5 | public email: string, 6 | public username: string, 7 | public password: string, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/users/interfaces/accounts-users.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AccountsUsers { 2 | readonly id: number; 3 | readonly name: string; 4 | readonly username: string; 5 | readonly email: string; 6 | readonly password: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/users/models/users.model.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Users { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | name: string; 10 | 11 | @Column() 12 | username: string; 13 | 14 | @Column({ 15 | unique: true, 16 | }) 17 | email: string; 18 | 19 | @Column({ length: 255 }) 20 | password: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/users/repositories/implementations/users.typeorm.repository.ts: -------------------------------------------------------------------------------- 1 | import { Users } from '../../../users/models/users.model'; 2 | import { UsersRepository } from '../users.repository.interface'; 3 | import { Repository, UpdateResult } from 'typeorm'; 4 | import { UserProfileDto } from '../../../users/dto/user-profile.dto'; 5 | import { UserUpdateDto } from '../../../users/dto/user-update.dto'; 6 | import { UserDto } from '../../../users/dto/user.dto'; 7 | import { HashingService } from '../../../common/hashing/hashing.service'; 8 | import { AccountsUsers } from '../../../users/interfaces/accounts-users.interface'; 9 | 10 | export class UsersTypeOrmRepository implements UsersRepository { 11 | constructor( 12 | private readonly usersRepository: Repository, 13 | private readonly hashingService: HashingService, 14 | ) {} 15 | 16 | public async findAll() { 17 | return await this.usersRepository.find(); 18 | } 19 | 20 | public async findByEmail(email: string) { 21 | return await this.usersRepository.findOneBy({ 22 | email: email, 23 | }); 24 | } 25 | 26 | public async findBySub(sub: number): Promise { 27 | return await this.usersRepository.findOneByOrFail({ 28 | id: sub, 29 | }); 30 | } 31 | 32 | public async findById(userId: string): Promise { 33 | return await this.usersRepository.findOneBy({ 34 | id: +userId, 35 | }); 36 | } 37 | 38 | public async create(userDto: UserDto): Promise { 39 | return await this.usersRepository.save(userDto); 40 | } 41 | 42 | public async updateByEmail(email: string): Promise { 43 | const user = await this.usersRepository.findOneBy({ email: email }); 44 | user.password = await this.hashingService.hash( 45 | Math.random().toString(36).slice(-8), 46 | ); 47 | 48 | return await this.usersRepository.save(user); 49 | } 50 | 51 | public async updateByPassword( 52 | email: string, 53 | password: string, 54 | ): Promise { 55 | const user = await this.usersRepository.findOneBy({ email: email }); 56 | user.password = await this.hashingService.hash(password); 57 | 58 | return await this.usersRepository.save(user); 59 | } 60 | 61 | public async updateUserProfile( 62 | id: string, 63 | userProfileDto: UserProfileDto, 64 | ): Promise { 65 | const user = await this.usersRepository.findOneBy({ id: +id }); 66 | user.name = userProfileDto.name; 67 | user.email = userProfileDto.email; 68 | user.username = userProfileDto.username; 69 | 70 | return await this.usersRepository.save(user); 71 | } 72 | 73 | public async updateUser( 74 | id: string, 75 | userUpdateDto: UserUpdateDto, 76 | ): Promise { 77 | return await this.usersRepository.update( 78 | { 79 | id: +id, 80 | }, 81 | { ...userUpdateDto }, 82 | ); 83 | } 84 | 85 | public async deleteUser(user: any): Promise { 86 | await this.usersRepository.remove(user); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/users/repositories/users.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { UserProfileDto } from '../dto/user-profile.dto'; 2 | import { UserUpdateDto } from '../dto/user-update.dto'; 3 | import { UserDto } from '../dto/user.dto'; 4 | 5 | export interface UsersRepository { 6 | findAll(): void; 7 | findByEmail(email: string): void; 8 | findBySub(sub: number): void; 9 | findById(userId: string): void; 10 | create(userDto: UserDto): void; 11 | updateByEmail(email: string): void; 12 | updateByPassword(email: string, password: string): void; 13 | updateUserProfile(id: string, userProfileDto: UserProfileDto): void; 14 | updateUser(id: string, userUpdateDto: UserUpdateDto): void; 15 | deleteUser(id: string): void; 16 | } 17 | 18 | export const USERS_REPOSITORY_TOKEN = 'users-repository-token'; 19 | -------------------------------------------------------------------------------- /src/users/repositories/users.repository.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Provider } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { DataSource } from '../../constants'; 4 | import { Repository } from 'typeorm'; 5 | import { USERS_REPOSITORY_TOKEN } from './users.repository.interface'; 6 | import { UsersTypeOrmRepository } from './implementations/users.typeorm.repository'; 7 | import { Users } from '../models/users.model'; 8 | import { HashingService } from '../../common/hashing/hashing.service'; 9 | 10 | export function provideUsersRepository(): Provider[] { 11 | return [ 12 | { 13 | provide: USERS_REPOSITORY_TOKEN, 14 | useFactory: (dependenciesProvider: UsersRepoDependenciesProvider) => 15 | provideUsersRepositoryFactory(dependenciesProvider), 16 | inject: [UsersRepoDependenciesProvider], 17 | }, 18 | UsersRepoDependenciesProvider, 19 | ]; 20 | } 21 | 22 | function provideUsersRepositoryFactory( 23 | dependenciesProvider: UsersRepoDependenciesProvider, 24 | ) { 25 | const dataSourceEnv = process.env.USERS_DATASOURCE; 26 | 27 | if ( 28 | !dataSourceEnv || 29 | !Object.values(DataSource).includes(dataSourceEnv as DataSource) 30 | ) { 31 | throw new Error(`Invalid USERS_DATASOURCE: ${dataSourceEnv}`); 32 | } 33 | 34 | const dataSource = dataSourceEnv as DataSource; 35 | 36 | switch (dataSource) { 37 | case DataSource.TYPEORM: 38 | return new UsersTypeOrmRepository( 39 | dependenciesProvider.typeOrmRepository, 40 | dependenciesProvider.hashingService, 41 | ); 42 | } 43 | } 44 | 45 | @Injectable() 46 | export class UsersRepoDependenciesProvider { 47 | constructor( 48 | @InjectRepository(Users) 49 | public typeOrmRepository: Repository, 50 | public hashingService: HashingService, 51 | ) {} 52 | } 53 | -------------------------------------------------------------------------------- /src/users/users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, NotFoundException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { UserProfileDto } from './dto/user-profile.dto'; 4 | import { UserDto } from './dto/user.dto'; 5 | import { UsersController } from './users.controller'; 6 | import { UsersService } from './users.service'; 7 | 8 | const userDto: UserDto = { 9 | name: 'name #1', 10 | username: 'username #1', 11 | email: 'test@example.com', 12 | password: 'password123', 13 | }; 14 | 15 | const userUpdateDto: UserDto = { 16 | name: 'name #1 update', 17 | username: 'username #1 update', 18 | email: 'test@example.com', 19 | password: 'password123', 20 | }; 21 | 22 | const userProfileDto: UserProfileDto = { 23 | name: 'name #1', 24 | username: 'username #1', 25 | email: 'test@example.com', 26 | }; 27 | 28 | describe('Users Controller', () => { 29 | let usersController: UsersController; 30 | let usersService: UsersService; 31 | 32 | beforeEach(async () => { 33 | const module: TestingModule = await Test.createTestingModule({ 34 | controllers: [UsersController], 35 | providers: [ 36 | { 37 | provide: UsersService, 38 | useValue: { 39 | findAll: jest.fn(() => {}), 40 | findById: jest.fn(() => userDto), 41 | updateUserProfile: jest.fn(() => {}), 42 | updateUser: jest.fn(() => {}), 43 | deleteUser: jest.fn(() => userDto), 44 | }, 45 | }, 46 | ], 47 | }).compile(); 48 | 49 | usersController = module.get(UsersController); 50 | usersService = module.get(UsersService); 51 | }); 52 | 53 | describe('Users Controller', () => { 54 | it('should be defined', () => { 55 | expect(usersController).toBeDefined(); 56 | }); 57 | 58 | describe('findAllUser() method', () => { 59 | it('should call method findAllUser in userService', async () => { 60 | const createSpy = jest.spyOn(usersService, 'findAll'); 61 | 62 | await usersController.findAllUser(); 63 | expect(createSpy).toHaveBeenCalled(); 64 | }); 65 | }); 66 | 67 | describe('findOneUser() method', () => { 68 | it('should call method findOneUser in userService', async () => { 69 | const createSpy = jest.spyOn(usersService, 'findById'); 70 | 71 | await usersController.findOneUser('anyid'); 72 | expect(createSpy).toHaveBeenCalledWith('anyid'); 73 | }); 74 | }); 75 | 76 | describe('findById() method', () => { 77 | it('should call method getUser in userService', async () => { 78 | const createSpy = jest.spyOn(usersService, 'findById'); 79 | 80 | await usersController.getUser('1'); 81 | expect(createSpy).toHaveBeenCalledWith('1'); 82 | }); 83 | 84 | it('should return an exception if update user fails', async () => { 85 | usersService.findById = jest.fn().mockResolvedValueOnce(null); 86 | await expect(usersController.getUser('not correct id')).rejects.toThrow( 87 | NotFoundException, 88 | ); 89 | }); 90 | }); 91 | 92 | describe('updateUserProfile() method', () => { 93 | it('should call method updateProfileUser in userService', async () => { 94 | const createSpy = jest.spyOn(usersService, 'updateUserProfile'); 95 | 96 | await usersController.updateUserProfile('1', userProfileDto); 97 | expect(createSpy).toHaveBeenCalledWith('1', userProfileDto); 98 | }); 99 | 100 | it('should return an exception if update profile user fails', async () => { 101 | usersService.updateUserProfile = jest.fn().mockRejectedValueOnce(null); 102 | await expect( 103 | usersController.updateUserProfile('not a correct id', { 104 | name: 'not a correct name', 105 | username: 'not a correct username', 106 | email: 'not a correct email', 107 | }), 108 | ).rejects.toThrow(BadRequestException); 109 | }); 110 | }); 111 | 112 | describe('updateUser() method', () => { 113 | it('should call method updateUser in userService', async () => { 114 | const createSpy = jest.spyOn(usersService, 'updateUser'); 115 | 116 | await usersController.updateUser('1', userUpdateDto); 117 | expect(createSpy).toHaveBeenCalledWith('1', userUpdateDto); 118 | }); 119 | 120 | it('should return an exception if update user fails', async () => { 121 | usersService.updateUser = jest.fn().mockRejectedValueOnce(null); 122 | await expect( 123 | usersController.updateUser('not a correct id', { 124 | name: 'not a correct name', 125 | username: 'not a correct username', 126 | email: 'not a correct email', 127 | password: 'not a correct password', 128 | }), 129 | ).rejects.toThrow(BadRequestException); 130 | }); 131 | }); 132 | 133 | describe('deleteUser() method', () => { 134 | it('should call method deleteUser in userService', async () => { 135 | const createSpy = jest.spyOn(usersService, 'deleteUser'); 136 | 137 | await usersController.deleteUser('anyid'); 138 | expect(createSpy).toHaveBeenCalledWith('anyid'); 139 | }); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Put, 4 | Get, 5 | Body, 6 | Param, 7 | HttpStatus, 8 | NotFoundException, 9 | Delete, 10 | BadRequestException, 11 | } from '@nestjs/common'; 12 | import { UsersService } from './users.service'; 13 | import { UserProfileDto } from './dto/user-profile.dto'; 14 | import { UserUpdateDto } from './dto/user-update.dto'; 15 | import { AccountsUsers } from './interfaces/accounts-users.interface'; 16 | import { 17 | ApiBadRequestResponse, 18 | ApiBearerAuth, 19 | ApiNoContentResponse, 20 | ApiNotFoundResponse, 21 | ApiResponse, 22 | ApiTags, 23 | } from '@nestjs/swagger'; 24 | import { AuthGuard } from '../iam/login/decorators/auth-guard.decorator'; 25 | import { AuthType } from '../iam/login/enums/auth-type.enum'; 26 | 27 | interface GetUserResponse { 28 | user: AccountsUsers; 29 | status: number; 30 | } 31 | 32 | interface UpdateResponse { 33 | message: string; 34 | status: number; 35 | } 36 | 37 | @ApiTags('users') 38 | @ApiBearerAuth() 39 | @AuthGuard(AuthType.Bearer) 40 | @Controller('users') 41 | export class UsersController { 42 | constructor(private readonly usersService: UsersService) {} 43 | 44 | @Get() 45 | @ApiResponse({ status: 200, description: 'Get all users' }) 46 | public async findAllUser(): Promise { 47 | return this.usersService.findAll(); 48 | } 49 | 50 | @Get('/:userId') 51 | @ApiResponse({ status: 200, description: 'Get a user by id' }) 52 | @ApiNotFoundResponse({ description: 'User not found' }) 53 | public async findOneUser( 54 | @Param('userId') userId: string, 55 | ): Promise { 56 | return this.usersService.findById(userId); 57 | } 58 | 59 | @Get('/:userId/profile') 60 | @ApiResponse({ status: 200, description: 'Get a user profile by id' }) 61 | @ApiNotFoundResponse({ description: 'User not found' }) 62 | public async getUser( 63 | @Param('userId') userId: string, 64 | ): Promise { 65 | const user = await this.findOneUser(userId); 66 | 67 | if (!user) { 68 | throw new NotFoundException('User does not exist!'); 69 | } 70 | 71 | return { 72 | user, 73 | status: HttpStatus.OK, 74 | }; 75 | } 76 | 77 | @Put('/:userId/profile') 78 | @ApiResponse({ status: 200, description: 'Update a user profile by id' }) 79 | @ApiBadRequestResponse({ description: 'User profile not updated' }) 80 | public async updateUserProfile( 81 | @Param('userId') userId: string, 82 | @Body() userProfileDto: UserProfileDto, 83 | ): Promise { 84 | try { 85 | await this.usersService.updateUserProfile(userId, userProfileDto); 86 | 87 | return { 88 | message: 'User Updated successfully!', 89 | status: HttpStatus.OK, 90 | }; 91 | } catch (err) { 92 | throw new BadRequestException(err, 'Error: User not updated!'); 93 | } 94 | } 95 | 96 | @Put('/:userId') 97 | @ApiResponse({ status: 200, description: 'Update a user by id' }) 98 | @ApiBadRequestResponse({ description: 'User not updated' }) 99 | public async updateUser( 100 | @Param('userId') userId: string, 101 | @Body() userUpdateDto: UserUpdateDto, 102 | ): Promise { 103 | try { 104 | await this.usersService.updateUser(userId, userUpdateDto); 105 | 106 | return { 107 | message: 'User Updated successfully!', 108 | status: HttpStatus.OK, 109 | }; 110 | } catch (err) { 111 | throw new BadRequestException(err, 'Error: User not updated!'); 112 | } 113 | } 114 | 115 | @Delete('/:userId') 116 | @ApiResponse({ status: 200, description: 'Delete a user by id' }) 117 | @ApiNoContentResponse({ description: 'User not deleted' }) 118 | public async deleteUser(@Param('userId') userId: string): Promise { 119 | await this.usersService.deleteUser(userId); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Users } from './models/users.model'; 4 | import { UsersService } from './users.service'; 5 | import { UsersController } from './users.controller'; 6 | import { MailerModule } from '../common/mailer/mailer.module'; 7 | import { Argon2Service } from '../common/hashing/argon2.service'; 8 | import { HashingService } from '../common/hashing/hashing.service'; 9 | import { provideUsersRepository } from './repositories/users.repository.provider'; 10 | 11 | @Module({ 12 | imports: [TypeOrmModule.forFeature([Users]), MailerModule], 13 | controllers: [UsersController], 14 | providers: [ 15 | { 16 | provide: HashingService, 17 | useClass: Argon2Service, 18 | }, 19 | UsersService, 20 | ...provideUsersRepository(), 21 | ], 22 | }) 23 | export class UsersModule {} 24 | -------------------------------------------------------------------------------- /src/users/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, NotFoundException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { Argon2Service } from '../common/hashing/argon2.service'; 4 | import { HashingService } from '../common/hashing/hashing.service'; 5 | import { UserDto } from './dto/user.dto'; 6 | import { UsersService } from './users.service'; 7 | import { UsersTypeOrmRepository } from './repositories/implementations/users.typeorm.repository'; 8 | import { USERS_REPOSITORY_TOKEN } from './repositories/users.repository.interface'; 9 | 10 | const userArray = [ 11 | { 12 | id: 1, 13 | name: 'name #1', 14 | username: 'username #1', 15 | email: 'test1@example.com', 16 | password: 'pass123', 17 | }, 18 | { 19 | id: 2, 20 | name: 'name #2', 21 | username: 'username #2', 22 | email: 'test2@example.com', 23 | password: 'pass123', 24 | }, 25 | ]; 26 | 27 | const oneUser = { 28 | id: 1, 29 | name: 'name #1', 30 | username: 'username #1', 31 | email: 'test@example.com', 32 | password: 'pass123', 33 | }; 34 | 35 | const createUser: UserDto = { 36 | name: 'name #1', 37 | username: 'username #1', 38 | email: 'test@example.com', 39 | password: 'pass123', 40 | }; 41 | 42 | const updateUserByEmail = { 43 | name: 'name #1', 44 | username: 'username #1', 45 | email: 'test@example.com', 46 | password: 'pass123', 47 | }; 48 | 49 | const updateUserByPassword = { 50 | name: 'name #1', 51 | username: 'username #1', 52 | email: 'test@example.com', 53 | password: 'pass123', 54 | }; 55 | 56 | const updateUserProfile = { 57 | name: 'name #1', 58 | username: 'username #1', 59 | email: 'test@example.com', 60 | password: 'pass123', 61 | }; 62 | 63 | const updateUser = { 64 | id: 1, 65 | name: 'name #1 update', 66 | username: 'username #1 update', 67 | email: 'test@example.com', 68 | password: 'pass123', 69 | }; 70 | 71 | describe('UsersService', () => { 72 | let service: UsersService; 73 | let repository: UsersTypeOrmRepository; 74 | 75 | beforeEach(async () => { 76 | const module: TestingModule = await Test.createTestingModule({ 77 | providers: [ 78 | UsersService, 79 | { 80 | provide: HashingService, 81 | useClass: Argon2Service, 82 | }, 83 | { 84 | provide: USERS_REPOSITORY_TOKEN, 85 | useValue: { 86 | findAll: jest.fn().mockResolvedValue(userArray), 87 | findByEmail: jest.fn().mockResolvedValue(oneUser), 88 | findBySub: jest.fn().mockResolvedValueOnce(oneUser), 89 | findById: jest.fn().mockResolvedValueOnce(oneUser), 90 | create: jest.fn().mockReturnValue(createUser), 91 | updateByEmail: jest.fn().mockReturnValue(updateUserByEmail), 92 | updateByPassword: jest.fn().mockResolvedValue(updateUserByPassword), 93 | updateUserProfile: jest.fn().mockResolvedValue(updateUserProfile), 94 | updateUser: jest.fn().mockResolvedValue(updateUser), 95 | deleteUser: jest.fn(), 96 | }, 97 | }, 98 | ], 99 | }).compile(); 100 | 101 | service = module.get(UsersService); 102 | repository = module.get(USERS_REPOSITORY_TOKEN); 103 | }); 104 | 105 | it('should be defined', () => { 106 | expect(service).toBeDefined(); 107 | }); 108 | 109 | describe('findAll() method', () => { 110 | it('should return an array of all users', async () => { 111 | const users = await service.findAll(); 112 | expect(users).toEqual(userArray); 113 | }); 114 | }); 115 | 116 | describe('findByEmail() method', () => { 117 | it('should find a user by email', async () => { 118 | expect(await service.findByEmail('test@example.com')).toEqual(oneUser); 119 | }); 120 | 121 | it('should throw an exception if it not found a user by email', async () => { 122 | repository.findByEmail = jest.fn().mockResolvedValueOnce(null); 123 | await expect(service.findByEmail('not a correct email')).rejects.toThrow( 124 | NotFoundException, 125 | ); 126 | }); 127 | }); 128 | 129 | describe('findBySub() method', () => { 130 | it('should find a user by sub or fail', async () => { 131 | expect(await service.findBySub(1)).toEqual(oneUser); 132 | }); 133 | 134 | it('should throw an exception if it not found a user by sub', async () => { 135 | repository.findBySub = jest.fn().mockResolvedValueOnce(null); 136 | await expect(service.findBySub(1)).rejects.toThrow(NotFoundException); 137 | }); 138 | }); 139 | 140 | describe('findById() method', () => { 141 | it('should find a user by id', async () => { 142 | expect(await service.findById('anyid')).toEqual(oneUser); 143 | }); 144 | 145 | it('should throw an exception if it not found a user by id', async () => { 146 | repository.findById = jest.fn().mockResolvedValueOnce(null); 147 | await expect(service.findById('not a correct id')).rejects.toThrow( 148 | NotFoundException, 149 | ); 150 | }); 151 | }); 152 | 153 | describe('create() method', () => { 154 | it('should create a new user', async () => { 155 | expect( 156 | await service.create({ 157 | name: 'name #1', 158 | username: 'username #1', 159 | email: 'test@example.com', 160 | password: 'pass123', 161 | }), 162 | ).toEqual(createUser); 163 | }); 164 | 165 | it('should return an exception if login fails', async () => { 166 | repository.create = jest.fn().mockRejectedValueOnce(null); 167 | await expect( 168 | service.create({ 169 | name: 'not a correct name', 170 | username: 'not a correct username', 171 | email: 'not a correct email', 172 | password: 'not a correct password', 173 | }), 174 | ).rejects.toThrow(HttpException); 175 | }); 176 | }); 177 | 178 | describe('updateByEmail() method', () => { 179 | it('should update a user by email', async () => { 180 | expect(await service.updateByEmail('test@example.com')).toEqual( 181 | updateUserByEmail, 182 | ); 183 | }); 184 | 185 | it('should return an exception if update by email fails', async () => { 186 | repository.updateByEmail = jest.fn().mockRejectedValueOnce(null); 187 | await expect( 188 | service.updateByEmail('not a correct email'), 189 | ).rejects.toThrow(HttpException); 190 | }); 191 | }); 192 | 193 | describe('updateByPassword() method', () => { 194 | it('should update a user by password', async () => { 195 | expect( 196 | await service.updateByPassword('test@example.com', 'pass123'), 197 | ).toEqual(updateUserByPassword); 198 | }); 199 | 200 | it('should return an exception if update by password fails', async () => { 201 | repository.updateByPassword = jest.fn().mockRejectedValueOnce(null); 202 | await expect( 203 | service.updateByPassword('not a correct email', 'not correct password'), 204 | ).rejects.toThrow(HttpException); 205 | }); 206 | }); 207 | 208 | describe('updateUserProfile() method', () => { 209 | it('should update profile of a user by id', async () => { 210 | expect( 211 | await service.updateUserProfile('anyid', updateUserProfile), 212 | ).toEqual(updateUserProfile); 213 | }); 214 | 215 | it('should return an exception if update profile user fails', async () => { 216 | repository.updateUserProfile = jest.fn().mockRejectedValueOnce(null); 217 | await expect( 218 | service.updateUserProfile('not a correct id', { 219 | name: 'not a correct name', 220 | username: 'not a correct username', 221 | email: 'not a correct email', 222 | }), 223 | ).rejects.toThrow(HttpException); 224 | }); 225 | }); 226 | 227 | describe('updateUser() method', () => { 228 | it('should update a user by id', async () => { 229 | expect(await service.updateUser('anyid', updateUser)).toEqual(updateUser); 230 | }); 231 | 232 | it('should return an exception if update profile user fails', async () => { 233 | repository.updateUser = jest.fn().mockRejectedValueOnce(null); 234 | await expect( 235 | service.updateUser('not a correct id', { 236 | name: 'not a correct name', 237 | username: 'not a correct username', 238 | email: 'not a correct email', 239 | password: 'not a correct password', 240 | }), 241 | ).rejects.toThrow(HttpException); 242 | }); 243 | }); 244 | 245 | describe('deleteUser() method', () => { 246 | it('should remove a user by id', async () => { 247 | const removeSpy = jest.spyOn(repository, 'deleteUser'); 248 | const user = await service.deleteUser('any id'); 249 | expect(removeSpy).toHaveBeenCalledWith(oneUser); 250 | expect(user).toBeUndefined(); 251 | }); 252 | 253 | it('should throw an error if no user is found with an id', async () => { 254 | repository.findById = jest.fn().mockResolvedValueOnce(undefined); 255 | await expect(service.deleteUser('bad id')).rejects.toThrow( 256 | NotFoundException, 257 | ); 258 | expect(repository.findById).toHaveBeenCalledTimes(1); 259 | }); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NotFoundException, 4 | HttpException, 5 | HttpStatus, 6 | Inject, 7 | } from '@nestjs/common'; 8 | import { UpdateResult } from 'typeorm'; 9 | import { AccountsUsers } from './interfaces/accounts-users.interface'; 10 | import { Users } from './models/users.model'; 11 | import { UserDto } from './dto/user.dto'; 12 | import { UserProfileDto } from './dto/user-profile.dto'; 13 | import { UserUpdateDto } from './dto/user-update.dto'; 14 | import { USERS_REPOSITORY_TOKEN } from './repositories/users.repository.interface'; 15 | import { UsersTypeOrmRepository } from './repositories/implementations/users.typeorm.repository'; 16 | 17 | @Injectable() 18 | export class UsersService { 19 | constructor( 20 | @Inject(USERS_REPOSITORY_TOKEN) 21 | private readonly usersRepository: UsersTypeOrmRepository, 22 | ) {} 23 | 24 | public async findAll(): Promise { 25 | return await this.usersRepository.findAll(); 26 | } 27 | 28 | public async findByEmail(email: string): Promise { 29 | const user = await this.usersRepository.findByEmail(email); 30 | 31 | if (!user) { 32 | throw new NotFoundException(`User not found`); 33 | } 34 | 35 | return user; 36 | } 37 | 38 | public async findBySub(sub: number): Promise { 39 | const user = await this.usersRepository.findBySub(sub); 40 | 41 | if (!user) { 42 | throw new NotFoundException(`User not found`); 43 | } 44 | 45 | return user; 46 | } 47 | 48 | public async findById(userId: string): Promise { 49 | const user = await this.usersRepository.findById(userId); 50 | 51 | if (!user) { 52 | throw new NotFoundException(`User #${userId} not found`); 53 | } 54 | 55 | return user; 56 | } 57 | 58 | public async create(userDto: UserDto): Promise { 59 | try { 60 | return await this.usersRepository.create(userDto); 61 | } catch (err) { 62 | throw new HttpException(err, HttpStatus.BAD_REQUEST); 63 | } 64 | } 65 | 66 | public async updateByEmail(email: string): Promise { 67 | try { 68 | return await this.usersRepository.updateByEmail(email); 69 | } catch (err) { 70 | throw new HttpException(err, HttpStatus.BAD_REQUEST); 71 | } 72 | } 73 | 74 | public async updateByPassword( 75 | email: string, 76 | password: string, 77 | ): Promise { 78 | try { 79 | return await this.usersRepository.updateByPassword(email, password); 80 | } catch (err) { 81 | throw new HttpException(err, HttpStatus.BAD_REQUEST); 82 | } 83 | } 84 | 85 | public async updateUserProfile( 86 | id: string, 87 | userProfileDto: UserProfileDto, 88 | ): Promise { 89 | try { 90 | return await this.usersRepository.updateUserProfile(id, userProfileDto); 91 | } catch (err) { 92 | throw new HttpException(err, HttpStatus.BAD_REQUEST); 93 | } 94 | } 95 | 96 | public async updateUser( 97 | id: string, 98 | userUpdateDto: UserUpdateDto, 99 | ): Promise { 100 | try { 101 | return await this.usersRepository.updateUser(id, userUpdateDto); 102 | } catch (err) { 103 | throw new HttpException(err, HttpStatus.BAD_REQUEST); 104 | } 105 | } 106 | 107 | public async deleteUser(id: string): Promise { 108 | const user = await this.findById(id); 109 | return await this.usersRepository.deleteUser(user); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/app.module'; 4 | import { HttpStatus, ValidationPipe } from '@nestjs/common'; 5 | import { AccessTokenGuard } from '../src/iam/login/guards/access-token/access-token.guard'; 6 | import { 7 | FastifyAdapter, 8 | NestFastifyApplication, 9 | } from '@nestjs/platform-fastify'; 10 | 11 | describe('App (e2e)', () => { 12 | let app: NestFastifyApplication; 13 | let accessTokenJwt: string; 14 | let refreshTokenJwt: string; 15 | 16 | beforeAll(async () => { 17 | const moduleFixture: TestingModule = await Test.createTestingModule({ 18 | imports: [AppModule], 19 | }) 20 | .overrideGuard(AccessTokenGuard) 21 | .useValue({ canActivate: () => true }) 22 | .compile(); 23 | 24 | app = moduleFixture.createNestApplication( 25 | new FastifyAdapter() 26 | ); 27 | app.setGlobalPrefix('api'); 28 | app.useGlobalPipes( 29 | new ValidationPipe({ 30 | whitelist: true, 31 | transform: true, 32 | forbidNonWhitelisted: true, 33 | transformOptions: { 34 | enableImplicitConversion: true, 35 | }, 36 | }), 37 | ); 38 | await app.init(); 39 | await app.getHttpAdapter().getInstance().ready(); 40 | }); 41 | 42 | describe('AppController (e2e)', () => { 43 | it('should return the follwing message: "This is a simple example of item returned by your APIs." [GET /api]', () => { 44 | return request(app.getHttpServer()) 45 | .get('/api') 46 | .expect({ 47 | message: 'This is a simple example of item returned by your APIs.', 48 | }) 49 | .expect(HttpStatus.OK); 50 | }); 51 | 52 | describe('should sign in and get a "live" JWT', () => { 53 | it('should authenticates user with valid credentials and provides a jwt token', () => { 54 | return request(app.getHttpServer()) 55 | .post('/api/auth/login') 56 | .send({ 57 | email: 'test@example.com', 58 | password: 'pass123', 59 | }) 60 | .then(({ body }) => { 61 | accessTokenJwt = body.accessToken; 62 | refreshTokenJwt = body.refreshToken; 63 | 64 | expect(accessTokenJwt).toMatch( 65 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 66 | ); 67 | 68 | expect(refreshTokenJwt).toMatch( 69 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 70 | ); 71 | 72 | expect(body).toEqual({ 73 | accessToken: accessTokenJwt, 74 | refreshToken: refreshTokenJwt, 75 | user: { name: 'name #1', email: 'test@example.com', id: 1 }, 76 | }); 77 | 78 | expect(HttpStatus.OK); 79 | }); 80 | }); 81 | 82 | it('should return the follwing message: "Access to protected resources granted! This protected resource is displayed when the token is successfully provided". - ( endpoint protected ) [GET /api/secure]', () => { 83 | return request(app.getHttpServer()) 84 | .get('/api/secure') 85 | .set('Authorization', `Bearer ${accessTokenJwt}`) 86 | .expect({ 87 | message: 88 | 'Access to protected resources granted! This protected resource is displayed when the token is successfully provided.', 89 | }); 90 | }); 91 | }); 92 | }); 93 | 94 | afterAll(async () => { 95 | await app.close(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/change-password/change-password.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../../src/app.module'; 4 | import { MailerService } from '../../src/common/mailer/mailer.service'; 5 | import { 6 | BadRequestException, 7 | HttpStatus, 8 | ValidationPipe, 9 | } from '@nestjs/common'; 10 | import { AccessTokenGuard } from '../../src/iam/login/guards/access-token/access-token.guard'; 11 | import { 12 | FastifyAdapter, 13 | NestFastifyApplication, 14 | } from '@nestjs/platform-fastify'; 15 | 16 | const user = { 17 | email: 'test@example.com', 18 | password: 'pass123', 19 | }; 20 | 21 | describe('App (e2e)', () => { 22 | let app: NestFastifyApplication; 23 | let accessTokenJwt: string; 24 | let refreshTokenJwt: string; 25 | 26 | beforeAll(async () => { 27 | const moduleFixture: TestingModule = await Test.createTestingModule({ 28 | imports: [AppModule], 29 | }) 30 | .overrideProvider(MailerService) 31 | .useValue({ 32 | sendMail: jest.fn(() => true), 33 | }) 34 | .overrideGuard(AccessTokenGuard) 35 | .useValue({ canActivate: () => true }) 36 | .compile(); 37 | 38 | app = moduleFixture.createNestApplication( 39 | new FastifyAdapter() 40 | ); 41 | app.setGlobalPrefix('api'); 42 | app.useGlobalPipes( 43 | new ValidationPipe({ 44 | whitelist: true, 45 | transform: true, 46 | forbidNonWhitelisted: true, 47 | transformOptions: { 48 | enableImplicitConversion: true, 49 | }, 50 | }), 51 | ); 52 | 53 | await app.init(); 54 | await app.getHttpAdapter().getInstance().ready(); 55 | }); 56 | 57 | describe('should sign in and get a "live" JWT', () => { 58 | it('should authenticates user with valid credentials and provides a jwt token', () => { 59 | return request(app.getHttpServer()) 60 | .post('/api/auth/login') 61 | .send({ 62 | email: 'test@example.com', 63 | password: 'pass123', 64 | }) 65 | .then(({ body }) => { 66 | accessTokenJwt = body.accessToken; 67 | refreshTokenJwt = body.refreshToken; 68 | 69 | expect(accessTokenJwt).toMatch( 70 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 71 | ); 72 | 73 | expect(refreshTokenJwt).toMatch( 74 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 75 | ); 76 | 77 | expect(body).toEqual({ 78 | refreshToken: refreshTokenJwt, 79 | accessToken: accessTokenJwt, 80 | user: { name: 'name #1', email: 'test@example.com', id: 1 }, 81 | }); 82 | 83 | expect(HttpStatus.OK); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('ChangePasswordController (e2e) - [POST /api/auth/change-password]', () => { 89 | it('should change password an user', async () => { 90 | return await request(app.getHttpServer()) 91 | .post('/api/auth/change-password') 92 | .set('Authorization', `Bearer ${accessTokenJwt}`) 93 | .send(user) 94 | .then(({ body }) => { 95 | expect(body).toEqual({ 96 | message: 'Request Change Password Successfully!', 97 | status: 200, 98 | }); 99 | expect(HttpStatus.OK); 100 | }); 101 | }); 102 | }); 103 | 104 | it('should throw an error for a bad email', async () => { 105 | return await request(app.getHttpServer()) 106 | .post('/api/auth/change-password') 107 | .set('Authorization', `Bearer ${accessTokenJwt}`) 108 | .send({ 109 | password: 'new123456', 110 | }) 111 | .then(({ body }) => { 112 | expect(body).toEqual({ 113 | error: 'Bad Request', 114 | message: [ 115 | 'email should not be empty', 116 | 'email must be a string', 117 | 'email must be an email', 118 | ], 119 | statusCode: 400, 120 | }); 121 | expect(HttpStatus.BAD_REQUEST); 122 | expect(new BadRequestException()); 123 | }); 124 | }); 125 | 126 | it('should throw an error for a bad password', async () => { 127 | return await request(app.getHttpServer()) 128 | .post('/api/auth/change-password') 129 | .set('Authorization', `Bearer ${accessTokenJwt}`) 130 | .send({ 131 | email: 'test@example.it', 132 | }) 133 | .then(({ body }) => { 134 | expect(body).toEqual({ 135 | error: 'Bad Request', 136 | message: [ 137 | 'password must be shorter than or equal to 60 characters', 138 | 'password must be a string', 139 | 'password should not be empty', 140 | ], 141 | statusCode: 400, 142 | }); 143 | expect(HttpStatus.BAD_REQUEST); 144 | expect(new BadRequestException()); 145 | }); 146 | }); 147 | 148 | afterAll(async () => { 149 | await app.close(); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /test/forgot-password/forgot-password.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../../src/app.module'; 4 | import { MailerService } from '../../src/common/mailer/mailer.service'; 5 | import { 6 | BadRequestException, 7 | HttpStatus, 8 | ValidationPipe, 9 | } from '@nestjs/common'; 10 | import { ForgotPasswordDto } from 'src/iam/forgot-password/dto/forgot-password.dto'; 11 | import { UserDto } from '../../src/users/dto/user.dto'; 12 | import { 13 | FastifyAdapter, 14 | NestFastifyApplication, 15 | } from '@nestjs/platform-fastify'; 16 | 17 | const user = { 18 | email: 'test@example.com', 19 | }; 20 | 21 | const createUser = { 22 | name: 'name #1', 23 | username: 'username #1', 24 | email: 'test@example.com', 25 | password: 'pass123', 26 | }; 27 | 28 | describe('App (e2e)', () => { 29 | let app: NestFastifyApplication; 30 | 31 | beforeAll(async () => { 32 | const moduleFixture: TestingModule = await Test.createTestingModule({ 33 | imports: [AppModule], 34 | }) 35 | .overrideProvider(MailerService) 36 | .useValue({ 37 | sendMail: jest.fn(() => true), 38 | }) 39 | .compile(); 40 | 41 | app = moduleFixture.createNestApplication( 42 | new FastifyAdapter() 43 | ); 44 | app.setGlobalPrefix('api'); 45 | app.useGlobalPipes( 46 | new ValidationPipe({ 47 | whitelist: true, 48 | transform: true, 49 | forbidNonWhitelisted: true, 50 | transformOptions: { 51 | enableImplicitConversion: true, 52 | }, 53 | }), 54 | ); 55 | 56 | await app.init(); 57 | await app.getHttpAdapter().getInstance().ready(); 58 | }); 59 | 60 | describe('ForgotPassowrdController (e2e) - [POST /api/auth/forgot-password]', () => { 61 | it('should create user', async () => { 62 | return await request(app.getHttpServer()) 63 | .post('/api/auth/register') 64 | .send(createUser as UserDto) 65 | .then(({ body }) => { 66 | expect(body).toEqual({ 67 | message: 'User registration successfully!', 68 | status: 201, 69 | }); 70 | expect(HttpStatus.CREATED); 71 | }); 72 | }); 73 | 74 | it('should generate a new password per user if they have forgotten their password.', () => { 75 | return request(app.getHttpServer()) 76 | .post('/api/auth/forgot-password') 77 | .send(user as ForgotPasswordDto) 78 | .then(({ body }) => { 79 | expect(body).toEqual({ 80 | message: 'Request Reset Password Successfully!', 81 | status: 200, 82 | }); 83 | }); 84 | }); 85 | }); 86 | 87 | it('should throw an error for a bad email', () => { 88 | return request(app.getHttpServer()) 89 | .post('/api/auth/forgot-password') 90 | .send({ 91 | email: 'not correct', 92 | }) 93 | .then(({ body }) => { 94 | expect(body).toEqual({ 95 | error: 'Bad Request', 96 | message: ['email must be an email'], 97 | statusCode: 400, 98 | }); 99 | expect(HttpStatus.BAD_REQUEST); 100 | expect(new BadRequestException()); 101 | }); 102 | }); 103 | 104 | afterAll(async () => { 105 | await app.close(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/login/login.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../../src/app.module'; 4 | import { 5 | BadRequestException, 6 | HttpStatus, 7 | ValidationPipe, 8 | } from '@nestjs/common'; 9 | import { 10 | FastifyAdapter, 11 | NestFastifyApplication, 12 | } from '@nestjs/platform-fastify'; 13 | 14 | describe('App (e2e)', () => { 15 | let app: NestFastifyApplication; 16 | 17 | beforeAll(async () => { 18 | const moduleFixture: TestingModule = await Test.createTestingModule({ 19 | imports: [AppModule], 20 | }).compile(); 21 | 22 | app = moduleFixture.createNestApplication( 23 | new FastifyAdapter() 24 | ); 25 | app.setGlobalPrefix('api'); 26 | app.useGlobalPipes( 27 | new ValidationPipe({ 28 | whitelist: true, 29 | transform: true, 30 | forbidNonWhitelisted: true, 31 | transformOptions: { 32 | enableImplicitConversion: true, 33 | }, 34 | }), 35 | ); 36 | 37 | await app.init(); 38 | await app.getHttpAdapter().getInstance().ready(); 39 | }); 40 | 41 | describe('LoginController (e2e) - [POST /api/auth/login]', () => { 42 | let accessTokenJwt: string; 43 | let refreshTokenJwt: string; 44 | 45 | it('should authenticates user with valid credentials and provides a jwt token', () => { 46 | return request(app.getHttpServer()) 47 | .post('/api/auth/login') 48 | .send({ 49 | email: 'test@example.com', 50 | password: 'pass123', 51 | }) 52 | .then(({ body }) => { 53 | accessTokenJwt = body.accessToken; 54 | refreshTokenJwt = body.refreshToken; 55 | expect(accessTokenJwt).toMatch( 56 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 57 | ); 58 | 59 | expect(refreshTokenJwt).toMatch( 60 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 61 | ); 62 | 63 | expect(body).toEqual({ 64 | refreshToken: refreshTokenJwt, 65 | accessToken: accessTokenJwt, 66 | user: { name: 'name #1', email: 'test@example.com', id: 1 }, 67 | }); 68 | 69 | expect(HttpStatus.OK); 70 | }); 71 | }); 72 | 73 | it('should refresh token by jwt token used', () => { 74 | return request(app.getHttpServer()) 75 | .post('/api/auth/refresh-tokens') 76 | .send({ 77 | refreshToken: `${accessTokenJwt}`, 78 | }) 79 | .then(({ body }) => { 80 | accessTokenJwt = body.accessToken; 81 | refreshTokenJwt = body.refreshToken; 82 | expect(accessTokenJwt).toMatch( 83 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 84 | ); 85 | 86 | expect(refreshTokenJwt).toMatch( 87 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 88 | ); 89 | 90 | expect(body).toEqual({ 91 | refreshToken: refreshTokenJwt, 92 | accessToken: accessTokenJwt, 93 | user: { name: 'name #1', email: 'test@example.com', id: 1 }, 94 | }); 95 | 96 | expect(HttpStatus.OK); 97 | }); 98 | }); 99 | 100 | it('should fail if the token passed for refresh is incorrect', async () => { 101 | const response = await request(app.getHttpServer()) 102 | .post('/api/auth/refresh-tokens') 103 | .send({ 104 | refreshTokes: 'token wrong', 105 | }) 106 | .expect(HttpStatus.BAD_REQUEST); 107 | 108 | expect(response.body.accessToken).not.toBeDefined(); 109 | }); 110 | 111 | it('should fails to authenticate user with an incorrect password', async () => { 112 | const response = await request(app.getHttpServer()) 113 | .post('/api/auth/login') 114 | .send({ email: 'test@example.com', password: 'wrong' }) 115 | .expect(HttpStatus.UNAUTHORIZED); 116 | 117 | expect(response.body.accessToken).not.toBeDefined(); 118 | }); 119 | 120 | it('should throw an error for a bad email', () => { 121 | return request(app.getHttpServer()) 122 | .post('/api/auth/login') 123 | .send({ 124 | password: 'pass123', 125 | }) 126 | .then(({ body }) => { 127 | expect(body).toEqual({ 128 | error: 'Bad Request', 129 | message: [ 130 | 'email should not be empty', 131 | 'email must be a string', 132 | 'email must be an email', 133 | ], 134 | statusCode: 400, 135 | }); 136 | expect(HttpStatus.BAD_REQUEST); 137 | expect(new BadRequestException()); 138 | }); 139 | }); 140 | 141 | it('should throw an error for a bad password', () => { 142 | return request(app.getHttpServer()) 143 | .post('/api/auth/login') 144 | .send({ 145 | email: 'test@example.it', 146 | }) 147 | .then(({ body }) => { 148 | expect(body).toEqual({ 149 | error: 'Bad Request', 150 | message: [ 151 | 'password must be shorter than or equal to 60 characters', 152 | 'password must be a string', 153 | 'password should not be empty', 154 | ], 155 | statusCode: 400, 156 | }); 157 | expect(HttpStatus.BAD_REQUEST); 158 | expect(new BadRequestException()); 159 | }); 160 | }); 161 | }); 162 | 163 | afterAll(async () => { 164 | await app.close(); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /test/register/register.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../../src/app.module'; 4 | import { MailerService } from '../../src/common/mailer/mailer.service'; 5 | import { 6 | BadRequestException, 7 | HttpStatus, 8 | ValidationPipe, 9 | } from '@nestjs/common'; 10 | import { UserDto } from '../../src/users/dto/user.dto'; 11 | import { HashingService } from '../../src/common/hashing/hashing.service'; 12 | import { 13 | FastifyAdapter, 14 | NestFastifyApplication, 15 | } from '@nestjs/platform-fastify'; 16 | 17 | const user = { 18 | name: 'name #1', 19 | username: 'username #1', 20 | email: 'test@example.com', 21 | password: 'pass123', 22 | }; 23 | 24 | describe('App (e2e)', () => { 25 | let app: NestFastifyApplication; 26 | 27 | beforeAll(async () => { 28 | const moduleFixture: TestingModule = await Test.createTestingModule({ 29 | imports: [AppModule], 30 | providers: [ 31 | { 32 | provide: HashingService, 33 | useValue: { 34 | hash: jest.fn(() => 'pass123'), 35 | }, 36 | }, 37 | ], 38 | }) 39 | .overrideProvider(MailerService) 40 | .useValue({ 41 | sendMail: jest.fn(() => true), 42 | }) 43 | .compile(); 44 | 45 | app = moduleFixture.createNestApplication( 46 | new FastifyAdapter() 47 | ); 48 | app.setGlobalPrefix('api'); 49 | app.useGlobalPipes( 50 | new ValidationPipe({ 51 | whitelist: true, 52 | transform: true, 53 | forbidNonWhitelisted: true, 54 | transformOptions: { 55 | enableImplicitConversion: true, 56 | }, 57 | }), 58 | ); 59 | 60 | await app.init(); 61 | await app.getHttpAdapter().getInstance().ready(); 62 | }); 63 | 64 | describe('RegisterController (e2e) - [POST /api/auth/register]', () => { 65 | it('should register user', async () => { 66 | return await request(app.getHttpServer()) 67 | .post('/api/auth/register') 68 | .send(user as UserDto) 69 | .then(({ body }) => { 70 | expect(body).toEqual({ 71 | message: 'User registration successfully!', 72 | status: 201, 73 | }); 74 | expect(HttpStatus.CREATED); 75 | }); 76 | }); 77 | 78 | it('should throw an error for a bad email', async () => { 79 | return await request(app.getHttpServer()) 80 | .post('/api/auth/register') 81 | .send({ 82 | name: 'name#1 register', 83 | username: 'username#1 register', 84 | password: '123456789', 85 | }) 86 | .then(({ body }) => { 87 | expect(body).toEqual({ 88 | error: 'Bad Request', 89 | message: [ 90 | 'email should not be empty', 91 | 'email must be a string', 92 | 'email must be an email', 93 | ], 94 | statusCode: 400, 95 | }); 96 | expect(HttpStatus.BAD_REQUEST); 97 | expect(new BadRequestException()); 98 | }); 99 | }); 100 | 101 | it('should throw an error for a bad name', async () => { 102 | return await request(app.getHttpServer()) 103 | .post('/api/auth/register') 104 | .send({ 105 | username: 'username#1 register', 106 | email: 'test@example.it', 107 | password: '123456789', 108 | }) 109 | .expect(HttpStatus.BAD_REQUEST) 110 | .then(({ body }) => { 111 | expect(body).toEqual({ 112 | error: 'Bad Request', 113 | message: [ 114 | 'name must be shorter than or equal to 30 characters', 115 | 'name must be a string', 116 | ], 117 | statusCode: 400, 118 | }); 119 | expect(new BadRequestException()); 120 | }); 121 | }); 122 | 123 | it('should throw an error for a bad username', async () => { 124 | return await request(app.getHttpServer()) 125 | .post('/api/auth/register') 126 | .send({ 127 | name: 'name#1 register', 128 | email: 'test@example.it', 129 | password: '123456789', 130 | }) 131 | .then(({ body }) => { 132 | expect(body).toEqual({ 133 | error: 'Bad Request', 134 | message: [ 135 | 'username must be shorter than or equal to 40 characters', 136 | 'username must be a string', 137 | ], 138 | statusCode: 400, 139 | }); 140 | expect(HttpStatus.BAD_REQUEST); 141 | expect(new BadRequestException()); 142 | }); 143 | }); 144 | 145 | it('should throw an error for a bad password', async () => { 146 | return await request(app.getHttpServer()) 147 | .post('/api/auth/register') 148 | .send({ 149 | name: 'name#1 register', 150 | username: 'username#1 register', 151 | email: 'test@example.it', 152 | }) 153 | .then(({ body }) => { 154 | expect(body).toEqual({ 155 | error: 'Bad Request', 156 | message: [ 157 | 'password must be shorter than or equal to 60 characters', 158 | 'password must be a string', 159 | 'password should not be empty', 160 | ], 161 | statusCode: 400, 162 | }); 163 | expect(HttpStatus.BAD_REQUEST); 164 | expect(new BadRequestException()); 165 | }); 166 | }); 167 | }); 168 | 169 | afterAll(async () => { 170 | await app.close(); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /test/users/users.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../../src/app.module'; 4 | import { MailerService } from '../../src/common/mailer/mailer.service'; 5 | import { HttpStatus, ValidationPipe } from '@nestjs/common'; 6 | import { AccessTokenGuard } from '../../src/iam/login/guards/access-token/access-token.guard'; 7 | import { 8 | FastifyAdapter, 9 | NestFastifyApplication, 10 | } from '@nestjs/platform-fastify'; 11 | 12 | const users = [ 13 | { 14 | id: 1, 15 | name: 'name #1', 16 | username: 'username #1', 17 | email: 'test1@example.com', 18 | password: 'pass123', 19 | }, 20 | ]; 21 | 22 | const updateProfileUserDto = { 23 | name: 'name#1 update', 24 | username: 'username#1 update', 25 | email: 'test@example.it', 26 | }; 27 | 28 | describe('App (e2e)', () => { 29 | let app: NestFastifyApplication; 30 | let accessTokenJwt: string; 31 | let refreshTokenJwt: string; 32 | 33 | beforeAll(async () => { 34 | const moduleFixture: TestingModule = await Test.createTestingModule({ 35 | imports: [AppModule], 36 | }) 37 | .overrideProvider(MailerService) 38 | .useValue({ 39 | sendMail: jest.fn(() => true), 40 | }) 41 | .overrideGuard(AccessTokenGuard) 42 | .useValue({ canActivate: () => true }) 43 | .compile(); 44 | 45 | app = moduleFixture.createNestApplication( 46 | new FastifyAdapter() 47 | ); 48 | app.setGlobalPrefix('api'); 49 | app.useGlobalPipes( 50 | new ValidationPipe({ 51 | whitelist: true, 52 | transform: true, 53 | forbidNonWhitelisted: true, 54 | transformOptions: { 55 | enableImplicitConversion: true, 56 | }, 57 | }), 58 | ); 59 | 60 | await app.init(); 61 | await app.getHttpAdapter().getInstance().ready(); 62 | }); 63 | 64 | describe('UserController (e2e)', () => { 65 | describe('should sign in and get a "live" JWT', () => { 66 | it('should authenticates user with valid credentials and provides a jwt token', () => { 67 | return request(app.getHttpServer()) 68 | .post('/api/auth/login') 69 | .send({ 70 | email: 'test@example.com', 71 | password: 'pass123', 72 | }) 73 | .then(({ body }) => { 74 | accessTokenJwt = body.accessToken; 75 | refreshTokenJwt = body.refreshToken; 76 | 77 | expect(accessTokenJwt).toMatch( 78 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 79 | ); 80 | 81 | expect(refreshTokenJwt).toMatch( 82 | /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/, 83 | ); 84 | 85 | expect(body).toEqual({ 86 | accessToken: accessTokenJwt, 87 | refreshToken: refreshTokenJwt, 88 | user: { name: 'name #1', email: 'test@example.com', id: 1 }, 89 | }); 90 | 91 | expect(HttpStatus.OK); 92 | }); 93 | }); 94 | }); 95 | describe('Get all users [GET /api/users]', () => { 96 | it('should get all users', async () => { 97 | return await request(app.getHttpServer()) 98 | .get('/api/users') 99 | .set('Authorization', `Bearer ${accessTokenJwt}`) 100 | .expect(HttpStatus.OK) 101 | .then(({ body }) => { 102 | expect(body).toEqual([ 103 | { 104 | id: 1, 105 | name: 'name #1', 106 | username: 'username #1', 107 | email: 'test@example.com', 108 | password: body[0].password, 109 | }, 110 | ]); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('Get one user [GET /api/users/:id]', () => { 116 | it('should get one user', async () => { 117 | return await request(app.getHttpServer()) 118 | .get('/api/users/1') 119 | .set('Authorization', `Bearer ${accessTokenJwt}`) 120 | .expect(HttpStatus.OK) 121 | .then(({ body }) => { 122 | expect(body).toEqual({ 123 | id: 1, 124 | name: 'name #1', 125 | username: 'username #1', 126 | email: 'test@example.com', 127 | password: body.password, 128 | }); 129 | }); 130 | }); 131 | 132 | it('should return an incorrect request if it does not find the id', async () => { 133 | return await request(app.getHttpServer()) 134 | .get('/api/users/30') 135 | .set('Authorization', `Bearer ${accessTokenJwt}`) 136 | .then(({ body }) => { 137 | expect(body).toEqual({ 138 | error: 'Not Found', 139 | message: 'User #30 not found', 140 | statusCode: HttpStatus.NOT_FOUND, 141 | }); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('Get one user profile [GET /api/users/:id/profile]', () => { 147 | it('should get one user profile', async () => { 148 | return await request(app.getHttpServer()) 149 | .get('/api/users/1/profile') 150 | .set('Authorization', `Bearer ${accessTokenJwt}`) 151 | .expect(HttpStatus.OK) 152 | .then(({ body }) => { 153 | expect(body).toEqual({ 154 | user: { 155 | id: 1, 156 | name: 'name #1', 157 | username: 'username #1', 158 | email: 'test@example.com', 159 | password: body.user.password, 160 | }, 161 | status: HttpStatus.OK, 162 | }); 163 | }); 164 | }); 165 | 166 | it('should return an incorrect request if it does not find the user profile id', async () => { 167 | return await request(app.getHttpServer()) 168 | .get('/api/users/20/profile') 169 | .set('Authorization', `Bearer ${accessTokenJwt}`) 170 | .expect(HttpStatus.NOT_FOUND); 171 | }); 172 | }); 173 | 174 | describe('Update one user profile [PUT /api/users/:id/profile]', () => { 175 | it('should update one user profile by id', async () => { 176 | return await request(app.getHttpServer()) 177 | .put('/api/users/1/profile') 178 | .set('Authorization', `Bearer ${accessTokenJwt}`) 179 | .send({ 180 | name: 'name #1', 181 | username: 'username #1', 182 | email: 'test@example.com', 183 | }) 184 | .expect(HttpStatus.OK) 185 | .then(({ body }) => { 186 | expect(body).toEqual({ 187 | message: 'User Updated successfully!', 188 | status: HttpStatus.OK, 189 | }); 190 | }); 191 | }); 192 | 193 | it('should return an incorrect request if it does not find the id', async () => { 194 | return await request(app.getHttpServer()) 195 | .put('/api/users/10/profile') 196 | .set('Authorization', `Bearer ${accessTokenJwt}`) 197 | .send(updateProfileUserDto) 198 | .expect(HttpStatus.BAD_REQUEST); 199 | }); 200 | }); 201 | // 202 | describe('Update one user [PUT /api/users/:id]', () => { 203 | it('should update one user', async () => { 204 | return await request(app.getHttpServer()) 205 | .put('/api/users/1') 206 | .set('Authorization', `Bearer ${accessTokenJwt}`) 207 | .send({ 208 | name: 'name #1', 209 | username: 'username #1', 210 | email: 'test@example.com', 211 | password: 212 | '$2b$10$hgJzgGh2tkqqIYpIYQI9pO0Q1S9Vd.OXnJcsm1oA1nYvd9yet8sxi', 213 | }) 214 | .expect(HttpStatus.OK) 215 | .then(({ body }) => { 216 | expect(body).toEqual({ 217 | message: 'User Updated successfully!', 218 | status: HttpStatus.OK, 219 | }); 220 | }); 221 | }); 222 | 223 | it('should return an incorrect request if it does not find the id', async () => { 224 | return await request(app.getHttpServer()) 225 | .put('/api/users/10') 226 | .set('Authorization', `Bearer ${accessTokenJwt}`) 227 | .send(null) 228 | .expect(HttpStatus.BAD_REQUEST); 229 | }); 230 | }); 231 | 232 | describe('Delete on user [DELETE /api/users/:id]', () => { 233 | it('should delete one user by id', async () => { 234 | return await request(app.getHttpServer()) 235 | .delete('/api/users/1') 236 | .set('Authorization', `Bearer ${accessTokenJwt}`) 237 | .expect(HttpStatus.OK) 238 | .then(() => { 239 | return request(app.getHttpServer()) 240 | .get('/users/1') 241 | .expect(HttpStatus.NOT_FOUND); 242 | }); 243 | }); 244 | 245 | it('should return an incorrect request if it does not find the id', () => { 246 | return request(app.getHttpServer()) 247 | .delete('/api/users/10') 248 | .set('Authorization', `Bearer ${accessTokenJwt}`) 249 | .expect(HttpStatus.NOT_FOUND); 250 | }); 251 | }); 252 | }); 253 | 254 | afterAll(async () => { 255 | await app.close(); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "ES2021", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "strict": true, 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": true, 17 | "strictBindCallApply": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "exclude": ["node_modules", "dist"] 25 | } 26 | -------------------------------------------------------------------------------- /typeorm-cli.config.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | import { config } from 'dotenv'; 3 | 4 | config(); 5 | 6 | export default new DataSource({ 7 | type: 'mysql', 8 | host: process.env.TYPEORM_HOST, 9 | port: process.env.TYPEORM_PORT 10 | ? parseInt(process.env.TYPEORM_PORT, 10) 11 | : 3306, 12 | username: process.env.TYPEORM_USERNAME, 13 | password: process.env.TYPEORM_PASSWORD, 14 | database: process.env.TYPEORM_DATABASE, 15 | entities: [], 16 | migrations: [], 17 | }); 18 | --------------------------------------------------------------------------------