├── .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 |
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 |
--------------------------------------------------------------------------------