├── .env.example ├── .env.test.example ├── .gitignore ├── README.md ├── db ├── .gitkeep └── init │ └── create-user.sh ├── docker-compose.test.yml ├── docker-compose.yml ├── gateway ├── .dockerignore ├── .eslintrc.js ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package.json ├── src │ ├── app.module.ts │ ├── decorators │ │ ├── authorization.decorator.ts │ │ └── permission.decorator.ts │ ├── interfaces │ │ ├── common │ │ │ └── authorized-request.interface.ts │ │ ├── task │ │ │ ├── dto │ │ │ │ ├── create-task-response.dto.ts │ │ │ │ ├── create-task.dto.ts │ │ │ │ ├── delete-task-response.dto.ts │ │ │ │ ├── get-tasks-response.dto.ts │ │ │ │ ├── task-id.dto.ts │ │ │ │ ├── update-task-response.dto.ts │ │ │ │ └── update-task.dto.ts │ │ │ ├── service-task-create-response.interface.ts │ │ │ ├── service-task-delete-response.interface.ts │ │ │ ├── service-task-search-by-user-id-response.interface.ts │ │ │ ├── service-task-update-by-id-response.interface.ts │ │ │ └── task.interface.ts │ │ ├── token │ │ │ ├── service-token-create-response.interface.ts │ │ │ ├── service-token-destroy-response.interface.ts │ │ │ └── token-info.interface.ts │ │ └── user │ │ │ ├── dto │ │ │ ├── confirm-user-response.dto.ts │ │ │ ├── confirm-user.dto.ts │ │ │ ├── create-user-response.dto.ts │ │ │ ├── create-user.dto.ts │ │ │ ├── get-user-by-token-response.dto.ts │ │ │ ├── login-user-response.dto.ts │ │ │ ├── login-user.dto.ts │ │ │ └── logout-user-response.dto.ts │ │ │ ├── service-user-confirm-response.interface.ts │ │ │ ├── service-user-create-response.interface.ts │ │ │ ├── service-user-get-by-id-response.interface.ts │ │ │ ├── service-user-search-response.interface.ts │ │ │ └── user.interface.ts │ ├── main.ts │ ├── services │ │ ├── config │ │ │ └── config.service.ts │ │ └── guards │ │ │ ├── authorization.guard.ts │ │ │ └── permission.guard.ts │ ├── tasks.controller.ts │ └── users.controller.ts ├── test │ ├── config.ts │ ├── jest-e2e.json │ ├── mocks │ │ ├── task-create-request-success.mock.ts │ │ ├── task-update-request-success.mock.ts │ │ ├── user-login-request-fail.mock.ts │ │ ├── user-signup-request-fail.mock.ts │ │ └── user-signup-request-success.mock.ts │ ├── task.e2e-spec.ts │ ├── timeout.ts │ ├── user-confirm.e2e-spec.ts │ ├── user-sign-in.e2e-spec.ts │ ├── user-sign-out.e2e-spec.ts │ └── user-sign-up.e2e-spec.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json ├── mailer ├── .dockerignore ├── .eslintrc.js ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package.json ├── src │ ├── interfaces │ │ ├── email-data.interface.ts │ │ └── mail-send-response.interface.ts │ ├── mailer.controller.ts │ ├── mailer.module.ts │ ├── main.ts │ └── services │ │ └── config │ │ ├── config.service.ts │ │ └── mailer-config.service.ts ├── tsconfig.build.json ├── tsconfig.json └── tslint.json ├── package.json ├── permission ├── .dockerignore ├── .eslintrc.js ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package.json ├── src │ ├── constants │ │ └── permissions.ts │ ├── interfaces │ │ ├── permission-check-response.interface.ts │ │ ├── permission-strategy.interface.ts │ │ └── user.interface.ts │ ├── main.ts │ ├── permission.controller.ts │ ├── permission.module.ts │ └── services │ │ ├── config │ │ └── config.service.ts │ │ └── confirmed-strategy.service.ts ├── tsconfig.build.json └── tsconfig.json ├── task ├── .dockerignore ├── .eslintrc.js ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package.json ├── src │ ├── interfaces │ │ ├── task-create-response.interface.ts │ │ ├── task-delete-response.interface.ts │ │ ├── task-search-by-user-response.interface.ts │ │ ├── task-update-by-id-response.interface.ts │ │ ├── task-update-params.interface.ts │ │ └── task.interface.ts │ ├── main.ts │ ├── schemas │ │ └── task.schema.ts │ ├── services │ │ ├── config │ │ │ ├── config.service.ts │ │ │ └── mongo-config.service.ts │ │ └── task.service.ts │ ├── task.controller.ts │ └── task.module.ts ├── tsconfig.build.json └── tsconfig.json ├── token ├── .dockerignore ├── .eslintrc.js ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package.json ├── src │ ├── interfaces │ │ ├── token-data-response.interface.ts │ │ ├── token-destroy-response.interface.ts │ │ ├── token-response.interface.ts │ │ └── token.interface.ts │ ├── main.ts │ ├── schemas │ │ └── token.schema.ts │ ├── services │ │ ├── config │ │ │ ├── config.service.ts │ │ │ ├── jwt-config.service.ts │ │ │ └── mongo-config.service.ts │ │ └── token.service.ts │ ├── token.controller.ts │ └── token.module.ts ├── tsconfig.build.json └── tsconfig.json └── user ├── .dockerignore ├── .eslintrc.js ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package.json ├── src ├── interfaces │ ├── user-confirm-response.interface.ts │ ├── user-create-response.interface.ts │ ├── user-link.interface.ts │ ├── user-search-response.interface.ts │ └── user.interface.ts ├── main.ts ├── schemas │ ├── user-link.schema.ts │ └── user.schema.ts ├── services │ ├── config │ │ ├── config.service.ts │ │ └── mongo-config.service.ts │ └── user.service.ts ├── user.controller.ts └── user.module.ts ├── tsconfig.build.json └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_DSN=mongodb://u1:somepw@db:27017/testdb 2 | MONGO_DATABASE=testdb 3 | MONGO_USER=u1 4 | MONGO_PASSWORD=somepw 5 | MONGO_ROOT_USER=admin 6 | MONGO_ROOT_PASSWORD=adminpw 7 | API_GATEWAY_PORT=8000 8 | TASK_SERVICE_PORT=8001 9 | TASK_SERVICE_HOST=task 10 | TOKEN_SERVICE_PORT=8002 11 | TOKEN_SERVICE_HOST=token 12 | USER_SERVICE_PORT=8003 13 | USER_SERVICE_HOST=user 14 | MAILER_SERVICE_PORT=8004 15 | MAILER_SERVICE_HOST=mailer 16 | PERMISSION_SERVICE_PORT=8005 17 | PERMISSION_SERVICE_HOST=permission 18 | BASE_URI=http://localhost 19 | MAILER_DISABLED=0 20 | MAILER_DSN=smtps://someusername@denrox.com:somepw@denrox.com 21 | MAILER_FROM="Test mail" 22 | -------------------------------------------------------------------------------- /.env.test.example: -------------------------------------------------------------------------------- 1 | MONGO_DSN=mongodb://db:27017/testdb_test 2 | MONGO_DATABASE=testdb_test 3 | MONGO_USER= 4 | MONGO_PASSWORD= 5 | MONGO_ROOT_USER= 6 | MONGO_ROOT_PASSWORD= 7 | API_GATEWAY_PORT=8000 8 | TASK_SERVICE_PORT=8001 9 | TASK_SERVICE_HOST=127.0.0.1 10 | TOKEN_SERVICE_PORT=8002 11 | TOKEN_SERVICE_HOST=127.0.0.1 12 | USER_SERVICE_PORT=8003 13 | USER_SERVICE_HOST=127.0.0.1 14 | MAILER_SERVICE_PORT=8004 15 | MAILER_SERVICE_HOST=127.0.0.1 16 | PERMISSION_SERVICE_PORT=8005 17 | PERMISSION_SERVICE_HOST=127.0.0.1 18 | BASE_URI=http://localhost 19 | MAILER_DISABLED=1 20 | MAILER_DSN=smtps://someusername@denrox.com:somepw@denrox.com 21 | MAILER_FROM="Test mail" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist 3 | node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | coverage 17 | .nyc_output 18 | 19 | 20 | db/data 21 | .env 22 | .env.test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository shows how you can build API with microservice architecture using nestjs 2 | ## Features of this example 3 | This example is basically an API for some task manager application. It provides a possibility to perform sign up users, confirm user's emails, manage user's tasks. 4 | ## Running the example with docker-compose 5 | Execute `docker network create infrastructure && cp .env.example .env && docker-compose up -d` from the root of the repository 6 | ## Accessing the API itself and swagger docs for the API 7 | - Once you launch the API it will be accessible on port 8000. 8 | - Swagger docs for the API will be accessible locally via URI "**http://localhost:8000/api**" 9 | ## Launch services for integration testing (using docker-compose) 10 | - Execute `sudo vim /etc/hosts` and add line `127.0.0.1 db` to it. This is a temporary step, it will not be required since new update. 11 | - Execute `cp .env.example .env && cp .env.test.example .env.test` 12 | - Execute `docker-compose -f ./docker-compose.test.yml up -d` from the root of the repository 13 | - Run `cd ./gateway && npm install && npm run test` from the root of this repo 14 | ## Brief architecture overview 15 | This API showcase consists of the following parts: 16 | - API gateway 17 | - Token service - responsible for creating, decoding, destroying JWT tokens for users 18 | - User service - responsible for CRUD operations on users 19 | - Mailer service - responsible for sending out emails (confirm sign up) 20 | - Permission service - responsible for verifying permissions for logged in users. 21 | - Tasks service - responsible for CRUD operations on users tasks records 22 | - The service interact via **TCP sockets** 23 | 24 | This example uses a SINGLE database (MongoDB) instance for all microservices. **This is not a correct point, the correct way is to use a separate DB instance for every microservice.** I used one DB instance for all microservices to simplify this example. 25 | -------------------------------------------------------------------------------- /db/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BBeast131/nestjs-microservices-example/22751279f009ff86cb3ccea0574988a45fc0ad89/db/.gitkeep -------------------------------------------------------------------------------- /db/init/create-user.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mongo ${MONGO_INITDB_DATABASE} \ 3 | --host localhost \ 4 | --port 27017 \ 5 | -u ${MONGO_INITDB_ROOT_USERNAME} \ 6 | -p ${MONGO_INITDB_ROOT_PASSWORD} \ 7 | --authenticationDatabase admin \ 8 | --eval "db.createUser({user: '${MONGO_USER}', pwd: '${MONGO_PASSWORD}', roles:[{role:'readWrite', db: '${MONGO_INITDB_DATABASE}'}]});" 9 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | task: 4 | build: ./task 5 | restart: always 6 | hostname: task 7 | env_file: 8 | - .env.test 9 | networks: 10 | - backend 11 | links: 12 | - db 13 | ports: 14 | - ${TASK_SERVICE_PORT}:${TASK_SERVICE_PORT} 15 | token: 16 | build: ./token 17 | restart: always 18 | hostname: token 19 | env_file: 20 | - .env.test 21 | networks: 22 | - backend 23 | links: 24 | - db 25 | ports: 26 | - ${TOKEN_SERVICE_PORT}:${TOKEN_SERVICE_PORT} 27 | mailer: 28 | build: ./mailer 29 | restart: always 30 | hostname: mailer 31 | env_file: 32 | - .env.test 33 | networks: 34 | - backend 35 | ports: 36 | - ${MAILER_SERVICE_PORT}:${MAILER_SERVICE_PORT} 37 | permission: 38 | build: ./permission 39 | restart: always 40 | hostname: permission 41 | env_file: 42 | - .env.test 43 | networks: 44 | - backend 45 | ports: 46 | - ${PERMISSION_SERVICE_PORT}:${PERMISSION_SERVICE_PORT} 47 | user: 48 | build: ./user 49 | restart: always 50 | hostname: user 51 | env_file: 52 | - .env.test 53 | networks: 54 | - backend 55 | links: 56 | - mailer 57 | - db 58 | ports: 59 | - ${USER_SERVICE_PORT}:${USER_SERVICE_PORT} 60 | db: 61 | image: 'mongo:3.7' 62 | restart: always 63 | hostname: db 64 | environment: 65 | MONGO_INITDB_DATABASE: ${MONGO_DATABASE} 66 | volumes: 67 | - "./db/init/:/docker-entrypoint-initdb.d/" 68 | networks: 69 | - backend 70 | env_file: 71 | - .env.test 72 | ports: 73 | - 27017:27017 74 | networks: 75 | backend: 76 | driver: bridge 77 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | gateway: 4 | build: ./gateway 5 | restart: always 6 | hostname: gateway 7 | env_file: 8 | - .env 9 | ports: 10 | - "8000:8000" 11 | networks: 12 | - backend 13 | - frontend 14 | task: 15 | build: ./task 16 | restart: always 17 | hostname: task 18 | env_file: 19 | - .env 20 | networks: 21 | - backend 22 | links: 23 | - db 24 | token: 25 | build: ./token 26 | restart: always 27 | hostname: token 28 | env_file: 29 | - .env 30 | networks: 31 | - backend 32 | links: 33 | - db 34 | mailer: 35 | build: ./mailer 36 | restart: always 37 | hostname: mailer 38 | env_file: 39 | - .env 40 | networks: 41 | - backend 42 | permission: 43 | build: ./permission 44 | restart: always 45 | hostname: permission 46 | env_file: 47 | - .env 48 | networks: 49 | - backend 50 | user: 51 | build: ./user 52 | restart: always 53 | hostname: user 54 | env_file: 55 | - .env 56 | networks: 57 | - backend 58 | links: 59 | - mailer 60 | - db 61 | db: 62 | image: 'mongo:3.7' 63 | restart: always 64 | environment: 65 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER} 66 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD} 67 | MONGO_INITDB_DATABASE: ${MONGO_DATABASE} 68 | MONGO_USER: ${MONGO_USER} 69 | MONGO_PASSWORD: ${MONGO_PASSWORD} 70 | volumes: 71 | - "./db/data/db-files:/data/db" 72 | - "./db/init/:/docker-entrypoint-initdb.d/" 73 | ports: 74 | - 27017:27017 75 | networks: 76 | - backend 77 | networks: 78 | backend: 79 | driver: bridge 80 | frontend: 81 | external: 82 | name: infrastructure 83 | -------------------------------------------------------------------------------- /gateway/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /gateway/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /gateway/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0-alpine 2 | RUN npm install -g npm@6.14.7 3 | RUN mkdir -p /var/www/gateway 4 | WORKDIR /var/www/gateway 5 | ADD . /var/www/gateway 6 | RUN npm install 7 | CMD npm run build && npm run start:prod 8 | -------------------------------------------------------------------------------- /gateway/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gateway", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rimraf dist && tsc -p tsconfig.build.json", 9 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env\"", 10 | "start:test": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env.test\"", 11 | "start:prod": "node dist/main.js", 12 | "lint": "eslint \"{src,test}/**/*.ts\" --fix", 13 | "test": "jest --config ./test/jest-e2e.json --detectOpenHandles" 14 | }, 15 | "dependencies": { 16 | "@nestjs/common": "8.0.0", 17 | "@nestjs/core": "8.0.0", 18 | "@nestjs/microservices": "8.0.0", 19 | "@nestjs/platform-express": "8.0.0", 20 | "@nestjs/swagger": "5.0.9", 21 | "reflect-metadata": "0.1.13", 22 | "rimraf": "3.0.2", 23 | "rxjs": "7.3.0", 24 | "swagger-ui-express": "4.1.4" 25 | }, 26 | "devDependencies": { 27 | "@nestjs/testing": "8.0.0", 28 | "@types/express": "4.17.7", 29 | "@types/jest": "26.0.10", 30 | "@types/node": "14.0.27", 31 | "@types/supertest": "2.0.10", 32 | "dotenv": "8.2.0", 33 | "mongoose": "5.10.0", 34 | "jest": "26.4.0", 35 | "supertest": "4.0.2", 36 | "ts-jest": "26.2.0", 37 | "ts-node": "9.0.0", 38 | "tsc-watch": "4.2.9", 39 | "tsconfig-paths": "3.9.0", 40 | "typescript": "3.9.7", 41 | "prettier": "2.1.2", 42 | "eslint-config-prettier": "7.0.0", 43 | "eslint-plugin-prettier": "^3.1.4", 44 | "@typescript-eslint/eslint-plugin": "4.6.1", 45 | "@typescript-eslint/parser": "4.6.1", 46 | "eslint": "7.12.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gateway/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_GUARD } from '@nestjs/core'; 3 | import { ClientProxyFactory } from '@nestjs/microservices'; 4 | 5 | import { UsersController } from './users.controller'; 6 | import { TasksController } from './tasks.controller'; 7 | 8 | import { AuthGuard } from './services/guards/authorization.guard'; 9 | import { PermissionGuard } from './services/guards/permission.guard'; 10 | 11 | import { ConfigService } from './services/config/config.service'; 12 | 13 | @Module({ 14 | imports: [], 15 | controllers: [UsersController, TasksController], 16 | providers: [ 17 | ConfigService, 18 | { 19 | provide: 'TOKEN_SERVICE', 20 | useFactory: (configService: ConfigService) => { 21 | const tokenServiceOptions = configService.get('tokenService'); 22 | return ClientProxyFactory.create(tokenServiceOptions); 23 | }, 24 | inject: [ConfigService], 25 | }, 26 | { 27 | provide: 'USER_SERVICE', 28 | useFactory: (configService: ConfigService) => { 29 | const userServiceOptions = configService.get('userService'); 30 | return ClientProxyFactory.create(userServiceOptions); 31 | }, 32 | inject: [ConfigService], 33 | }, 34 | { 35 | provide: 'TASK_SERVICE', 36 | useFactory: (configService: ConfigService) => { 37 | return ClientProxyFactory.create(configService.get('taskService')); 38 | }, 39 | inject: [ConfigService], 40 | }, 41 | { 42 | provide: 'PERMISSION_SERVICE', 43 | useFactory: (configService: ConfigService) => { 44 | return ClientProxyFactory.create( 45 | configService.get('permissionService'), 46 | ); 47 | }, 48 | inject: [ConfigService], 49 | }, 50 | { 51 | provide: APP_GUARD, 52 | useClass: AuthGuard, 53 | }, 54 | { 55 | provide: APP_GUARD, 56 | useClass: PermissionGuard, 57 | }, 58 | ], 59 | }) 60 | export class AppModule {} 61 | -------------------------------------------------------------------------------- /gateway/src/decorators/authorization.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const Authorization = (secured: boolean) => 4 | SetMetadata('secured', secured); 5 | -------------------------------------------------------------------------------- /gateway/src/decorators/permission.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const Permission = (permission: string) => 4 | SetMetadata('permission', permission); 5 | -------------------------------------------------------------------------------- /gateway/src/interfaces/common/authorized-request.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '../user/user.interface'; 2 | 3 | export interface IAuthorizedRequest extends Request { 4 | user?: IUser; 5 | } 6 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/dto/create-task-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { ITask } from '../task.interface'; 3 | 4 | export class CreateTaskResponseDto { 5 | @ApiProperty({ example: 'task_create_success' }) 6 | message: string; 7 | @ApiProperty({ 8 | example: { 9 | task: { 10 | notification_id: null, 11 | name: 'test task', 12 | description: 'test task description', 13 | start_time: +new Date(), 14 | duration: 90000, 15 | user_id: '5d987c3bfb881ec86b476bca', 16 | is_solved: false, 17 | created_at: +new Date(), 18 | updated_at: +new Date(), 19 | id: '5d987c3bfb881ec86b476bcc', 20 | }, 21 | }, 22 | nullable: true, 23 | }) 24 | data: { 25 | task: ITask; 26 | }; 27 | @ApiProperty({ example: null, nullable: true }) 28 | errors: { [key: string]: any }; 29 | } 30 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/dto/create-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class CreateTaskDto { 4 | @ApiProperty({ example: 'test task' }) 5 | name: string; 6 | @ApiProperty({ example: 'test task description' }) 7 | description: string; 8 | @ApiProperty({ example: +new Date() }) 9 | start_time: number; 10 | @ApiProperty({ example: 90000 }) 11 | duration: number; 12 | } 13 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/dto/delete-task-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class DeleteTaskResponseDto { 4 | @ApiProperty({ example: 'task_delete_by_id_success' }) 5 | message: string; 6 | @ApiProperty({ example: null, nullable: true, type: 'null' }) 7 | data: null; 8 | @ApiProperty({ example: null, nullable: true }) 9 | errors: { [key: string]: any }; 10 | } 11 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/dto/get-tasks-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { ITask } from '../task.interface'; 3 | 4 | export class GetTasksResponseDto { 5 | @ApiProperty({ example: 'task_search_success' }) 6 | message: string; 7 | @ApiProperty({ 8 | example: { 9 | tasks: [ 10 | { 11 | notification_id: null, 12 | name: 'test task', 13 | description: 'test task description', 14 | start_time: +new Date(), 15 | duration: 90000, 16 | user_id: '5d987c3bfb881ec86b476bca', 17 | is_solved: false, 18 | created_at: +new Date(), 19 | updated_at: +new Date(), 20 | id: '5d987c3bfb881ec86b476bcc', 21 | }, 22 | ], 23 | }, 24 | nullable: true, 25 | }) 26 | data: { 27 | tasks: ITask[]; 28 | }; 29 | @ApiProperty({ example: 'null' }) 30 | errors: { [key: string]: any }; 31 | } 32 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/dto/task-id.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class TaskIdDto { 4 | @ApiProperty() 5 | id: string; 6 | } 7 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/dto/update-task-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { ITask } from '../task.interface'; 3 | 4 | export class UpdateTaskResponseDto { 5 | @ApiProperty({ example: 'task_update_by_id_success' }) 6 | message: string; 7 | @ApiProperty({ 8 | example: { 9 | task: { 10 | notification_id: null, 11 | name: 'test task', 12 | description: 'test task description', 13 | start_time: +new Date(), 14 | duration: 90000, 15 | user_id: '5d987c3bfb881ec86b476bca', 16 | is_solved: false, 17 | created_at: +new Date(), 18 | updated_at: +new Date(), 19 | id: '5d987c3bfb881ec86b476bcc', 20 | }, 21 | }, 22 | nullable: true, 23 | }) 24 | data: { 25 | task: ITask; 26 | }; 27 | @ApiProperty({ example: null, nullable: true }) 28 | errors: { [key: string]: any }; 29 | } 30 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/dto/update-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class UpdateTaskDto { 4 | @ApiProperty({ required: false, example: 'test task' }) 5 | name: string; 6 | @ApiProperty({ required: false, example: 'test task description' }) 7 | description: string; 8 | @ApiProperty({ required: false, example: +new Date() }) 9 | start_time: number; 10 | @ApiProperty({ required: false, example: 90000 }) 11 | duration: number; 12 | @ApiProperty({ required: false, example: true }) 13 | is_solved: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/service-task-create-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { ITask } from './task.interface'; 2 | 3 | export interface IServiceTaskCreateResponse { 4 | status: number; 5 | message: string; 6 | task: ITask | null; 7 | errors: { [key: string]: any }; 8 | } 9 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/service-task-delete-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IServiceTaskDeleteResponse { 2 | status: number; 3 | message: string; 4 | errors: { [key: string]: any }; 5 | } 6 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/service-task-search-by-user-id-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { ITask } from './task.interface'; 2 | 3 | export interface IServiceTaskSearchByUserIdResponse { 4 | status: number; 5 | message: string; 6 | tasks: ITask[]; 7 | } 8 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/service-task-update-by-id-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { ITask } from './task.interface'; 2 | 3 | export interface IServiceTaskUpdateByIdResponse { 4 | status: number; 5 | message: string; 6 | task: ITask | null; 7 | errors: { [key: string]: any }; 8 | } 9 | -------------------------------------------------------------------------------- /gateway/src/interfaces/task/task.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITask { 2 | name: string; 3 | description: string; 4 | start_time: number; 5 | duration: number; 6 | is_solved: boolean; 7 | notification_id: number; 8 | } 9 | -------------------------------------------------------------------------------- /gateway/src/interfaces/token/service-token-create-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IServiveTokenCreateResponse { 2 | status: number; 3 | token: string | null; 4 | message: string; 5 | errors: { [key: string]: any }; 6 | } 7 | -------------------------------------------------------------------------------- /gateway/src/interfaces/token/service-token-destroy-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IServiceTokenDestroyResponse { 2 | status: number; 3 | message: string; 4 | errors: { [key: string]: any }; 5 | } 6 | -------------------------------------------------------------------------------- /gateway/src/interfaces/token/token-info.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITokenInfo { 2 | data: { 3 | userId: string; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/confirm-user-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ConfirmUserResponseDto { 4 | @ApiProperty({ example: 'user_confirm_success' }) 5 | message: string; 6 | @ApiProperty({ example: null, nullable: true, type: 'null' }) 7 | data: null; 8 | @ApiProperty({ example: null, nullable: true }) 9 | errors: { [key: string]: any }; 10 | } 11 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/confirm-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ConfirmUserDto { 4 | @ApiProperty() 5 | link: string; 6 | } 7 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/create-user-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IUser } from '../user.interface'; 3 | 4 | export class CreateUserResponseDto { 5 | @ApiProperty({ example: 'user_create_success' }) 6 | message: string; 7 | @ApiProperty({ 8 | example: { 9 | user: { 10 | email: 'test@denrox.com', 11 | is_confirmed: false, 12 | id: '5d987c3bfb881ec86b476bcc', 13 | }, 14 | }, 15 | nullable: true, 16 | }) 17 | data: { 18 | user: IUser; 19 | token: string; 20 | }; 21 | @ApiProperty({ example: null, nullable: true }) 22 | errors: { [key: string]: any }; 23 | } 24 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class CreateUserDto { 4 | @ApiProperty({ 5 | uniqueItems: true, 6 | example: 'test1@denrox.com', 7 | }) 8 | email: string; 9 | @ApiProperty({ 10 | minLength: 6, 11 | example: 'test11', 12 | }) 13 | password: string; 14 | } 15 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/get-user-by-token-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IUser } from '../user.interface'; 3 | 4 | export class GetUserByTokenResponseDto { 5 | @ApiProperty({ example: 'user_get_by_id_success' }) 6 | message: string; 7 | @ApiProperty({ 8 | example: { 9 | user: { 10 | email: 'test@denrox.com', 11 | is_confirmed: true, 12 | id: '5d987c3bfb881ec86b476bcc', 13 | }, 14 | }, 15 | nullable: true, 16 | }) 17 | data: { 18 | user: IUser; 19 | }; 20 | @ApiProperty({ example: null, nullable: true }) 21 | errors: { [key: string]: any }; 22 | } 23 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/login-user-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class LoginUserResponseDto { 4 | @ApiProperty({ example: 'token_create_success' }) 5 | message: string; 6 | @ApiProperty({ 7 | example: { token: 'someEncodedToken' }, 8 | nullable: true, 9 | }) 10 | data: { 11 | token: string; 12 | }; 13 | @ApiProperty({ example: null, nullable: true }) 14 | errors: { [key: string]: any }; 15 | } 16 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/login-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class LoginUserDto { 4 | @ApiProperty({ example: 'test1@denrox.com' }) 5 | email: string; 6 | @ApiProperty({ example: 'test11' }) 7 | password: string; 8 | } 9 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/dto/logout-user-response.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class LogoutUserResponseDto { 4 | @ApiProperty({ example: 'token_destroy_success' }) 5 | message: string; 6 | @ApiProperty({ example: null, nullable: true, type: 'null' }) 7 | data: null; 8 | @ApiProperty({ example: null, nullable: true }) 9 | errors: { [key: string]: any }; 10 | } 11 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/service-user-confirm-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IServiceUserConfirmResponse { 2 | status: number; 3 | message: string; 4 | errors: { [key: string]: any }; 5 | } 6 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/service-user-create-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './user.interface'; 2 | 3 | export interface IServiceUserCreateResponse { 4 | status: number; 5 | message: string; 6 | user: IUser | null; 7 | errors: { [key: string]: any }; 8 | } 9 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/service-user-get-by-id-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './user.interface'; 2 | 3 | export interface IServiceUserGetByIdResponse { 4 | status: number; 5 | message: string; 6 | user: IUser | null; 7 | } 8 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/service-user-search-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './user.interface'; 2 | 3 | export interface IServiceUserSearchResponse { 4 | status: number; 5 | message: string; 6 | user: IUser | null; 7 | } 8 | -------------------------------------------------------------------------------- /gateway/src/interfaces/user/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: string; 3 | email: string; 4 | } 5 | -------------------------------------------------------------------------------- /gateway/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 3 | import { AppModule } from './app.module'; 4 | import { ConfigService } from './services/config/config.service'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | const options = new DocumentBuilder() 9 | .setTitle('API docs') 10 | .addTag('users') 11 | .addTag('tasks') 12 | .setVersion('1.0') 13 | .build(); 14 | const document = SwaggerModule.createDocument(app, options); 15 | SwaggerModule.setup('api', app, document); 16 | await app.listen(new ConfigService().get('port')); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /gateway/src/services/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from '@nestjs/microservices'; 2 | 3 | export class ConfigService { 4 | private readonly envConfig: { [key: string]: any } = null; 5 | 6 | constructor() { 7 | this.envConfig = {}; 8 | this.envConfig.port = process.env.API_GATEWAY_PORT; 9 | this.envConfig.tokenService = { 10 | options: { 11 | port: process.env.TOKEN_SERVICE_PORT, 12 | host: process.env.TOKEN_SERVICE_HOST, 13 | }, 14 | transport: Transport.TCP, 15 | }; 16 | this.envConfig.userService = { 17 | options: { 18 | port: process.env.USER_SERVICE_PORT, 19 | host: process.env.USER_SERVICE_HOST, 20 | }, 21 | transport: Transport.TCP, 22 | }; 23 | this.envConfig.taskService = { 24 | options: { 25 | port: process.env.TASK_SERVICE_PORT, 26 | host: process.env.TASK_SERVICE_HOST, 27 | }, 28 | transport: Transport.TCP, 29 | }; 30 | this.envConfig.permissionService = { 31 | options: { 32 | port: process.env.PERMISSION_SERVICE_PORT, 33 | host: process.env.PERMISSION_SERVICE_HOST, 34 | }, 35 | transport: Transport.TCP, 36 | }; 37 | } 38 | 39 | get(key: string): any { 40 | return this.envConfig[key]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gateway/src/services/guards/authorization.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | Inject, 4 | CanActivate, 5 | ExecutionContext, 6 | HttpException, 7 | } from '@nestjs/common'; 8 | import { firstValueFrom } from 'rxjs'; 9 | import { Reflector } from '@nestjs/core'; 10 | import { ClientProxy } from '@nestjs/microservices'; 11 | 12 | @Injectable() 13 | export class AuthGuard implements CanActivate { 14 | constructor( 15 | private readonly reflector: Reflector, 16 | @Inject('TOKEN_SERVICE') private readonly tokenServiceClient: ClientProxy, 17 | @Inject('USER_SERVICE') private readonly userServiceClient: ClientProxy, 18 | ) {} 19 | 20 | public async canActivate(context: ExecutionContext): Promise { 21 | const secured = this.reflector.get( 22 | 'secured', 23 | context.getHandler(), 24 | ); 25 | 26 | if (!secured) { 27 | return true; 28 | } 29 | 30 | const request = context.switchToHttp().getRequest(); 31 | const userTokenInfo = await firstValueFrom( 32 | this.tokenServiceClient.send('token_decode', { 33 | token: request.headers.authorization, 34 | }), 35 | ); 36 | 37 | if (!userTokenInfo || !userTokenInfo.data) { 38 | throw new HttpException( 39 | { 40 | message: userTokenInfo.message, 41 | data: null, 42 | errors: null, 43 | }, 44 | userTokenInfo.status, 45 | ); 46 | } 47 | 48 | const userInfo = await firstValueFrom( 49 | this.userServiceClient.send('user_get_by_id', userTokenInfo.data.userId), 50 | ); 51 | 52 | request.user = userInfo.user; 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /gateway/src/services/guards/permission.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | Inject, 4 | CanActivate, 5 | ExecutionContext, 6 | HttpException, 7 | HttpStatus, 8 | } from '@nestjs/common'; 9 | import { firstValueFrom } from 'rxjs'; 10 | import { Reflector } from '@nestjs/core'; 11 | import { ClientProxy } from '@nestjs/microservices'; 12 | 13 | @Injectable() 14 | export class PermissionGuard implements CanActivate { 15 | constructor( 16 | private readonly reflector: Reflector, 17 | @Inject('PERMISSION_SERVICE') 18 | private readonly permissionServiceClient: ClientProxy, 19 | ) {} 20 | 21 | public async canActivate(context: ExecutionContext): Promise { 22 | const permission = this.reflector.get( 23 | 'permission', 24 | context.getHandler(), 25 | ); 26 | 27 | if (!permission) { 28 | return true; 29 | } 30 | 31 | const request = context.switchToHttp().getRequest(); 32 | 33 | const permissionInfo = await firstValueFrom( 34 | this.permissionServiceClient.send('permission_check', { 35 | permission, 36 | user: request.user, 37 | }), 38 | ); 39 | 40 | if (!permissionInfo || permissionInfo.status !== HttpStatus.OK) { 41 | throw new HttpException( 42 | { 43 | message: permissionInfo.message, 44 | data: null, 45 | errors: null, 46 | }, 47 | permissionInfo.status, 48 | ); 49 | } 50 | 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /gateway/src/tasks.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Inject, 4 | Get, 5 | Post, 6 | Put, 7 | Delete, 8 | Param, 9 | Body, 10 | Req, 11 | HttpException, 12 | HttpStatus, 13 | } from '@nestjs/common'; 14 | import { firstValueFrom } from 'rxjs'; 15 | import { ClientProxy } from '@nestjs/microservices'; 16 | import { ApiTags, ApiOkResponse, ApiCreatedResponse } from '@nestjs/swagger'; 17 | 18 | import { Authorization } from './decorators/authorization.decorator'; 19 | import { Permission } from './decorators/permission.decorator'; 20 | 21 | import { IAuthorizedRequest } from './interfaces/common/authorized-request.interface'; 22 | import { IServiceTaskCreateResponse } from './interfaces/task/service-task-create-response.interface'; 23 | import { IServiceTaskDeleteResponse } from './interfaces/task/service-task-delete-response.interface'; 24 | import { IServiceTaskSearchByUserIdResponse } from './interfaces/task/service-task-search-by-user-id-response.interface'; 25 | import { IServiceTaskUpdateByIdResponse } from './interfaces/task/service-task-update-by-id-response.interface'; 26 | import { GetTasksResponseDto } from './interfaces/task/dto/get-tasks-response.dto'; 27 | import { CreateTaskResponseDto } from './interfaces/task/dto/create-task-response.dto'; 28 | import { DeleteTaskResponseDto } from './interfaces/task/dto/delete-task-response.dto'; 29 | import { UpdateTaskResponseDto } from './interfaces/task/dto/update-task-response.dto'; 30 | import { CreateTaskDto } from './interfaces/task/dto/create-task.dto'; 31 | import { UpdateTaskDto } from './interfaces/task/dto/update-task.dto'; 32 | import { TaskIdDto } from './interfaces/task/dto/task-id.dto'; 33 | 34 | @Controller('tasks') 35 | @ApiTags('tasks') 36 | export class TasksController { 37 | constructor( 38 | @Inject('TASK_SERVICE') private readonly taskServiceClient: ClientProxy, 39 | ) {} 40 | 41 | @Get() 42 | @Authorization(true) 43 | @Permission('task_search_by_user_id') 44 | @ApiOkResponse({ 45 | type: GetTasksResponseDto, 46 | description: 'List of tasks for signed in user', 47 | }) 48 | public async getTasks( 49 | @Req() request: IAuthorizedRequest, 50 | ): Promise { 51 | const userInfo = request.user; 52 | 53 | const tasksResponse: IServiceTaskSearchByUserIdResponse = await firstValueFrom( 54 | this.taskServiceClient.send('task_search_by_user_id', userInfo.id), 55 | ); 56 | 57 | return { 58 | message: tasksResponse.message, 59 | data: { 60 | tasks: tasksResponse.tasks, 61 | }, 62 | errors: null, 63 | }; 64 | } 65 | 66 | @Post() 67 | @Authorization(true) 68 | @Permission('task_create') 69 | @ApiCreatedResponse({ 70 | type: CreateTaskResponseDto, 71 | }) 72 | public async createTask( 73 | @Req() request: IAuthorizedRequest, 74 | @Body() taskRequest: CreateTaskDto, 75 | ): Promise { 76 | const userInfo = request.user; 77 | const createTaskResponse: IServiceTaskCreateResponse = await firstValueFrom( 78 | this.taskServiceClient.send( 79 | 'task_create', 80 | Object.assign(taskRequest, { user_id: userInfo.id }), 81 | ), 82 | ); 83 | 84 | if (createTaskResponse.status !== HttpStatus.CREATED) { 85 | throw new HttpException( 86 | { 87 | message: createTaskResponse.message, 88 | data: null, 89 | errors: createTaskResponse.errors, 90 | }, 91 | createTaskResponse.status, 92 | ); 93 | } 94 | 95 | return { 96 | message: createTaskResponse.message, 97 | data: { 98 | task: createTaskResponse.task, 99 | }, 100 | errors: null, 101 | }; 102 | } 103 | 104 | @Delete(':id') 105 | @Authorization(true) 106 | @Permission('task_delete_by_id') 107 | @ApiOkResponse({ 108 | type: DeleteTaskResponseDto, 109 | }) 110 | public async deleteTask( 111 | @Req() request: IAuthorizedRequest, 112 | @Param() params: TaskIdDto, 113 | ): Promise { 114 | const userInfo = request.user; 115 | 116 | const deleteTaskResponse: IServiceTaskDeleteResponse = await firstValueFrom( 117 | this.taskServiceClient.send('task_delete_by_id', { 118 | id: params.id, 119 | userId: userInfo.id, 120 | }), 121 | ); 122 | 123 | if (deleteTaskResponse.status !== HttpStatus.OK) { 124 | throw new HttpException( 125 | { 126 | message: deleteTaskResponse.message, 127 | errors: deleteTaskResponse.errors, 128 | data: null, 129 | }, 130 | deleteTaskResponse.status, 131 | ); 132 | } 133 | 134 | return { 135 | message: deleteTaskResponse.message, 136 | data: null, 137 | errors: null, 138 | }; 139 | } 140 | 141 | @Put(':id') 142 | @Authorization(true) 143 | @Permission('task_update_by_id') 144 | @ApiOkResponse({ 145 | type: UpdateTaskResponseDto, 146 | }) 147 | public async updateTask( 148 | @Req() request: IAuthorizedRequest, 149 | @Param() params: TaskIdDto, 150 | @Body() taskRequest: UpdateTaskDto, 151 | ): Promise { 152 | const userInfo = request.user; 153 | const updateTaskResponse: IServiceTaskUpdateByIdResponse = await firstValueFrom( 154 | this.taskServiceClient.send('task_update_by_id', { 155 | id: params.id, 156 | userId: userInfo.id, 157 | task: taskRequest, 158 | }), 159 | ); 160 | 161 | if (updateTaskResponse.status !== HttpStatus.OK) { 162 | throw new HttpException( 163 | { 164 | message: updateTaskResponse.message, 165 | errors: updateTaskResponse.errors, 166 | data: null, 167 | }, 168 | updateTaskResponse.status, 169 | ); 170 | } 171 | 172 | return { 173 | message: updateTaskResponse.message, 174 | data: { 175 | task: updateTaskResponse.task, 176 | }, 177 | errors: null, 178 | }; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /gateway/src/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Put, 5 | Get, 6 | Body, 7 | Req, 8 | Inject, 9 | HttpStatus, 10 | HttpException, 11 | Param, 12 | } from '@nestjs/common'; 13 | import { firstValueFrom } from 'rxjs'; 14 | import { ClientProxy } from '@nestjs/microservices'; 15 | import { ApiTags, ApiOkResponse, ApiCreatedResponse } from '@nestjs/swagger'; 16 | 17 | import { Authorization } from './decorators/authorization.decorator'; 18 | import { IAuthorizedRequest } from './interfaces/common/authorized-request.interface'; 19 | import { IServiceUserCreateResponse } from './interfaces/user/service-user-create-response.interface'; 20 | import { IServiceUserSearchResponse } from './interfaces/user/service-user-search-response.interface'; 21 | import { IServiveTokenCreateResponse } from './interfaces/token/service-token-create-response.interface'; 22 | import { IServiceTokenDestroyResponse } from './interfaces/token/service-token-destroy-response.interface'; 23 | import { IServiceUserConfirmResponse } from './interfaces/user/service-user-confirm-response.interface'; 24 | import { IServiceUserGetByIdResponse } from './interfaces/user/service-user-get-by-id-response.interface'; 25 | 26 | import { GetUserByTokenResponseDto } from './interfaces/user/dto/get-user-by-token-response.dto'; 27 | import { CreateUserDto } from './interfaces/user/dto/create-user.dto'; 28 | import { CreateUserResponseDto } from './interfaces/user/dto/create-user-response.dto'; 29 | import { LoginUserDto } from './interfaces/user/dto/login-user.dto'; 30 | import { LoginUserResponseDto } from './interfaces/user/dto/login-user-response.dto'; 31 | import { LogoutUserResponseDto } from './interfaces/user/dto/logout-user-response.dto'; 32 | import { ConfirmUserDto } from './interfaces/user/dto/confirm-user.dto'; 33 | import { ConfirmUserResponseDto } from './interfaces/user/dto/confirm-user-response.dto'; 34 | 35 | @Controller('users') 36 | @ApiTags('users') 37 | export class UsersController { 38 | constructor( 39 | @Inject('TOKEN_SERVICE') private readonly tokenServiceClient: ClientProxy, 40 | @Inject('USER_SERVICE') private readonly userServiceClient: ClientProxy, 41 | ) {} 42 | 43 | @Get() 44 | @Authorization(true) 45 | @ApiOkResponse({ 46 | type: GetUserByTokenResponseDto, 47 | }) 48 | public async getUserByToken( 49 | @Req() request: IAuthorizedRequest, 50 | ): Promise { 51 | const userInfo = request.user; 52 | 53 | const userResponse: IServiceUserGetByIdResponse = await firstValueFrom( 54 | this.userServiceClient.send('user_get_by_id', userInfo.id), 55 | ); 56 | 57 | return { 58 | message: userResponse.message, 59 | data: { 60 | user: userResponse.user, 61 | }, 62 | errors: null, 63 | }; 64 | } 65 | 66 | @Post() 67 | @ApiCreatedResponse({ 68 | type: CreateUserResponseDto, 69 | }) 70 | public async createUser( 71 | @Body() userRequest: CreateUserDto, 72 | ): Promise { 73 | const createUserResponse: IServiceUserCreateResponse = await firstValueFrom( 74 | this.userServiceClient.send('user_create', userRequest), 75 | ); 76 | if (createUserResponse.status !== HttpStatus.CREATED) { 77 | throw new HttpException( 78 | { 79 | message: createUserResponse.message, 80 | data: null, 81 | errors: createUserResponse.errors, 82 | }, 83 | createUserResponse.status, 84 | ); 85 | } 86 | 87 | const createTokenResponse: IServiveTokenCreateResponse = await firstValueFrom( 88 | this.tokenServiceClient.send('token_create', { 89 | userId: createUserResponse.user.id, 90 | }), 91 | ); 92 | 93 | return { 94 | message: createUserResponse.message, 95 | data: { 96 | user: createUserResponse.user, 97 | token: createTokenResponse.token, 98 | }, 99 | errors: null, 100 | }; 101 | } 102 | 103 | @Post('/login') 104 | @ApiCreatedResponse({ 105 | type: LoginUserResponseDto, 106 | }) 107 | public async loginUser( 108 | @Body() loginRequest: LoginUserDto, 109 | ): Promise { 110 | const getUserResponse: IServiceUserSearchResponse = await firstValueFrom( 111 | this.userServiceClient.send('user_search_by_credentials', loginRequest), 112 | ); 113 | 114 | if (getUserResponse.status !== HttpStatus.OK) { 115 | throw new HttpException( 116 | { 117 | message: getUserResponse.message, 118 | data: null, 119 | errors: null, 120 | }, 121 | HttpStatus.UNAUTHORIZED, 122 | ); 123 | } 124 | 125 | const createTokenResponse: IServiveTokenCreateResponse = await firstValueFrom( 126 | this.tokenServiceClient.send('token_create', { 127 | userId: getUserResponse.user.id, 128 | }), 129 | ); 130 | 131 | return { 132 | message: createTokenResponse.message, 133 | data: { 134 | token: createTokenResponse.token, 135 | }, 136 | errors: null, 137 | }; 138 | } 139 | 140 | @Put('/logout') 141 | @Authorization(true) 142 | @ApiCreatedResponse({ 143 | type: LogoutUserResponseDto, 144 | }) 145 | public async logoutUser( 146 | @Req() request: IAuthorizedRequest, 147 | ): Promise { 148 | const userInfo = request.user; 149 | 150 | const destroyTokenResponse: IServiceTokenDestroyResponse = await firstValueFrom( 151 | this.tokenServiceClient.send('token_destroy', { 152 | userId: userInfo.id, 153 | }), 154 | ); 155 | 156 | if (destroyTokenResponse.status !== HttpStatus.OK) { 157 | throw new HttpException( 158 | { 159 | message: destroyTokenResponse.message, 160 | data: null, 161 | errors: destroyTokenResponse.errors, 162 | }, 163 | destroyTokenResponse.status, 164 | ); 165 | } 166 | 167 | return { 168 | message: destroyTokenResponse.message, 169 | errors: null, 170 | data: null, 171 | }; 172 | } 173 | 174 | @Get('/confirm/:link') 175 | @ApiCreatedResponse({ 176 | type: ConfirmUserResponseDto, 177 | }) 178 | public async confirmUser( 179 | @Param() params: ConfirmUserDto, 180 | ): Promise { 181 | const confirmUserResponse: IServiceUserConfirmResponse = await firstValueFrom( 182 | this.userServiceClient.send('user_confirm', { 183 | link: params.link, 184 | }), 185 | ); 186 | 187 | if (confirmUserResponse.status !== HttpStatus.OK) { 188 | throw new HttpException( 189 | { 190 | message: confirmUserResponse.message, 191 | data: null, 192 | errors: confirmUserResponse.errors, 193 | }, 194 | confirmUserResponse.status, 195 | ); 196 | } 197 | 198 | return { 199 | message: confirmUserResponse.message, 200 | errors: null, 201 | data: null, 202 | }; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /gateway/test/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config({ 3 | path: '../.env.test', 4 | }); 5 | -------------------------------------------------------------------------------- /gateway/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 | "setupFiles": ["./config.ts"], 10 | "setupFilesAfterEnv": ["./timeout.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /gateway/test/mocks/task-create-request-success.mock.ts: -------------------------------------------------------------------------------- 1 | export const taskCreateRequestSuccess = { 2 | name: 'test task', 3 | description: 'some description for the test task', 4 | start_time: 1569357602567, 5 | duration: 900000, 6 | }; 7 | -------------------------------------------------------------------------------- /gateway/test/mocks/task-update-request-success.mock.ts: -------------------------------------------------------------------------------- 1 | export const taskUpdateRequestSuccess = { 2 | name: 'test task (updated)', 3 | description: 'some description for the test task (updated)', 4 | start_time: 1569357602568, 5 | duration: 900001, 6 | is_solved: true, 7 | }; 8 | -------------------------------------------------------------------------------- /gateway/test/mocks/user-login-request-fail.mock.ts: -------------------------------------------------------------------------------- 1 | import { userSignupRequestSuccess } from './user-signup-request-success.mock'; 2 | 3 | export const userLoginRequestFailWrongPw = { 4 | ...userSignupRequestSuccess, 5 | password: new Date(), 6 | }; 7 | 8 | export const userLoginRequestFailWrongEmail = { 9 | ...userSignupRequestSuccess, 10 | email: 'failed' + userSignupRequestSuccess.email, 11 | }; 12 | -------------------------------------------------------------------------------- /gateway/test/mocks/user-signup-request-fail.mock.ts: -------------------------------------------------------------------------------- 1 | export const userSignupRequestFailShortPw = { 2 | email: 'test@denrox.com', 3 | password: 'test1', 4 | }; 5 | 6 | export const userSignupRequestFailNoPw = { 7 | email: 'test@denrox.com', 8 | }; 9 | 10 | export const userSignupRequestFailInvalidEmail = { 11 | email: 'testdenrox.com', 12 | password: 'test11', 13 | }; 14 | -------------------------------------------------------------------------------- /gateway/test/mocks/user-signup-request-success.mock.ts: -------------------------------------------------------------------------------- 1 | export const userSignupRequestSuccess = { 2 | email: 'test@denrox.com', 3 | password: 'test111', 4 | }; 5 | -------------------------------------------------------------------------------- /gateway/test/task.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import * as mongoose from 'mongoose'; 4 | import { AppModule } from './../src/app.module'; 5 | import { userSignupRequestSuccess } from './mocks/user-signup-request-success.mock'; 6 | import { taskCreateRequestSuccess } from './mocks/task-create-request-success.mock'; 7 | import { taskUpdateRequestSuccess } from './mocks/task-update-request-success.mock'; 8 | 9 | describe('Tasks (e2e)', () => { 10 | let app; 11 | let user; 12 | let taskId: string; 13 | let userToken: string; 14 | 15 | beforeAll(async () => { 16 | await mongoose.connect(process.env.MONGO_DSN, { useNewUrlParser: true }); 17 | await mongoose.connection.dropDatabase(); 18 | }); 19 | 20 | beforeEach(async () => { 21 | const moduleFixture: TestingModule = await Test.createTestingModule({ 22 | imports: [AppModule], 23 | }).compile(); 24 | 25 | app = moduleFixture.createNestApplication(); 26 | await app.init(); 27 | }); 28 | 29 | it('/users/ (POST) - should create a user for checking tasks api', (done) => { 30 | return request(app.getHttpServer()) 31 | .post('/users/') 32 | .send(userSignupRequestSuccess) 33 | .expect(201) 34 | .end(done); 35 | }); 36 | 37 | it('/users/login (POST) - should create a token for valid credentials', (done) => { 38 | return request(app.getHttpServer()) 39 | .post('/users/login') 40 | .send(userSignupRequestSuccess) 41 | .expect(201) 42 | .expect((res) => { 43 | userToken = res.body.data.token; 44 | }) 45 | .end(done); 46 | }); 47 | 48 | it('/tasks (GET) - should not return tasks without valid token', (done) => { 49 | return request(app.getHttpServer()) 50 | .get('/tasks') 51 | .expect(401) 52 | .expect({ 53 | message: 'token_decode_unauthorized', 54 | data: null, 55 | errors: null, 56 | }) 57 | .end(done); 58 | }); 59 | 60 | it('/tasks (POST) - should not create a task without a valid token', (done) => { 61 | return request(app.getHttpServer()) 62 | .post('/tasks') 63 | .expect(401) 64 | .expect({ 65 | message: 'token_decode_unauthorized', 66 | data: null, 67 | errors: null, 68 | }) 69 | .end(done); 70 | }); 71 | 72 | it('/tasks (POST) - should not create a task with an invalid token', (done) => { 73 | return request(app.getHttpServer()) 74 | .post('/tasks') 75 | .set('Authorization', userToken + 1) 76 | .send(taskCreateRequestSuccess) 77 | .expect(401) 78 | .expect({ 79 | message: 'token_decode_unauthorized', 80 | data: null, 81 | errors: null, 82 | }) 83 | .end(done); 84 | }); 85 | 86 | it('/tasks (POST) - should not create a task for an unconfirmed user with valid token', (done) => { 87 | return request(app.getHttpServer()) 88 | .post('/tasks') 89 | .set('Authorization', userToken) 90 | .send(taskCreateRequestSuccess) 91 | .expect(403) 92 | .expect({ 93 | message: 'permission_check_forbidden', 94 | data: null, 95 | errors: null, 96 | }) 97 | .end(done); 98 | }); 99 | 100 | it('/tasks (GET) - should not retrieve tasks without a valid token', (done) => { 101 | return request(app.getHttpServer()) 102 | .get('/tasks') 103 | .expect(401) 104 | .expect({ 105 | message: 'token_decode_unauthorized', 106 | data: null, 107 | errors: null, 108 | }) 109 | .end(done); 110 | }); 111 | 112 | it('/tasks (GET) - should not retrieve tasks with an valid token', (done) => { 113 | return request(app.getHttpServer()) 114 | .get('/tasks') 115 | .set('Authorization', userToken + 1) 116 | .expect(401) 117 | .expect({ 118 | message: 'token_decode_unauthorized', 119 | data: null, 120 | errors: null, 121 | }) 122 | .end(done); 123 | }); 124 | 125 | it('/tasks (POST) - should not retrieve tasks for an unconfirmed user with valid token', (done) => { 126 | return request(app.getHttpServer()) 127 | .get('/tasks') 128 | .set('Authorization', userToken) 129 | .expect(403) 130 | .expect({ 131 | message: 'permission_check_forbidden', 132 | data: null, 133 | errors: null, 134 | }) 135 | .end(done); 136 | }); 137 | 138 | it('/users/confirm/:link (GET) - should confirm user', async () => { 139 | user = await mongoose.connection 140 | .collection('users') 141 | .find({ 142 | email: userSignupRequestSuccess.email, 143 | }) 144 | .toArray(); 145 | const userConfirmation = await mongoose.connection 146 | .collection('user_links') 147 | .find({ 148 | user_id: user[0]._id.toString(), 149 | }) 150 | .toArray(); 151 | 152 | return request(app.getHttpServer()) 153 | .get(`/users/confirm/${userConfirmation[0].link}`) 154 | .send() 155 | .expect(200) 156 | .expect({ 157 | message: 'user_confirm_success', 158 | errors: null, 159 | data: null, 160 | }); 161 | }); 162 | 163 | it('/tasks (POST) - should create a task for the user with a valid token', (done) => { 164 | return request(app.getHttpServer()) 165 | .post('/tasks') 166 | .set('Authorization', userToken) 167 | .send(taskCreateRequestSuccess) 168 | .expect(201) 169 | .expect((res) => { 170 | taskId = res.body.data.task.id; 171 | res.body.data.task.id = 'fake_value'; 172 | res.body.data.task.created_at = 'fake_value'; 173 | res.body.data.task.updated_at = 'fake_value'; 174 | }) 175 | .expect({ 176 | message: 'task_create_success', 177 | data: { 178 | task: { 179 | notification_id: null, 180 | name: taskCreateRequestSuccess.name, 181 | description: taskCreateRequestSuccess.description, 182 | start_time: taskCreateRequestSuccess.start_time, 183 | duration: taskCreateRequestSuccess.duration, 184 | user_id: user[0]._id.toString(), 185 | is_solved: false, 186 | created_at: 'fake_value', 187 | updated_at: 'fake_value', 188 | id: 'fake_value', 189 | }, 190 | }, 191 | errors: null, 192 | }) 193 | .end(done); 194 | }); 195 | 196 | it('/tasks (POST) - should not create a task with invalid params', (done) => { 197 | return request(app.getHttpServer()) 198 | .post('/tasks') 199 | .set('Authorization', userToken) 200 | .send(null) 201 | .expect(412) 202 | .expect((res) => { 203 | res.body.errors.duration.properties = 'fake_properties'; 204 | res.body.errors.start_time.properties = 'fake_properties'; 205 | res.body.errors.name.properties = 'fake_properties'; 206 | }) 207 | .expect({ 208 | message: 'task_create_precondition_failed', 209 | data: null, 210 | errors: { 211 | duration: { 212 | message: 'Duration can not be empty', 213 | name: 'ValidatorError', 214 | properties: 'fake_properties', 215 | kind: 'required', 216 | path: 'duration', 217 | }, 218 | start_time: { 219 | message: 'Start time can not be empty', 220 | name: 'ValidatorError', 221 | properties: 'fake_properties', 222 | kind: 'required', 223 | path: 'start_time', 224 | }, 225 | name: { 226 | message: 'Name can not be empty', 227 | name: 'ValidatorError', 228 | properties: 'fake_properties', 229 | kind: 'required', 230 | path: 'name', 231 | }, 232 | }, 233 | }) 234 | .end(done); 235 | }); 236 | 237 | it('/tasks (GET) - should retrieve tasks for a valid token', (done) => { 238 | return request(app.getHttpServer()) 239 | .get('/tasks') 240 | .set('Authorization', userToken) 241 | .expect(200) 242 | .expect((res) => { 243 | res.body.data.tasks[0].created_at = 'fake_value'; 244 | res.body.data.tasks[0].updated_at = 'fake_value'; 245 | }) 246 | .expect({ 247 | message: 'task_search_by_user_id_success', 248 | data: { 249 | tasks: [ 250 | { 251 | notification_id: null, 252 | name: taskCreateRequestSuccess.name, 253 | description: taskCreateRequestSuccess.description, 254 | start_time: taskCreateRequestSuccess.start_time, 255 | duration: taskCreateRequestSuccess.duration, 256 | created_at: 'fake_value', 257 | updated_at: 'fake_value', 258 | user_id: user[0]._id.toString(), 259 | is_solved: false, 260 | id: taskId, 261 | }, 262 | ], 263 | }, 264 | errors: null, 265 | }) 266 | .end(done); 267 | }); 268 | 269 | it('/tasks/{id} (PUT) - should not task with invalid token', (done) => { 270 | return request(app.getHttpServer()) 271 | .put(`/tasks/${taskId}`) 272 | .send(taskUpdateRequestSuccess) 273 | .expect(401) 274 | .expect({ 275 | message: 'token_decode_unauthorized', 276 | data: null, 277 | errors: null, 278 | }) 279 | .end(done); 280 | }); 281 | 282 | it('/tasks/{id} (PUT) - should not update task with user_id param', (done) => { 283 | return request(app.getHttpServer()) 284 | .put(`/tasks/${taskId}`) 285 | .set('Authorization', userToken) 286 | .send({ 287 | ...taskUpdateRequestSuccess, 288 | user_id: user[0]._id.toString() + 1, 289 | }) 290 | .expect(412) 291 | .expect((res) => { 292 | res.body.errors.user_id.properties = 'fake_properties'; 293 | }) 294 | .expect({ 295 | message: 'task_update_by_id_precondition_failed', 296 | data: null, 297 | errors: { 298 | user_id: { 299 | message: 'The field value can not be updated', 300 | name: 'ValidatorError', 301 | properties: 'fake_properties', 302 | kind: 'user defined', 303 | path: 'user_id', 304 | }, 305 | }, 306 | }) 307 | .end(done); 308 | }); 309 | 310 | it('/tasks/{id} (PUT) - should update task with valid params', (done) => { 311 | return request(app.getHttpServer()) 312 | .put(`/tasks/${taskId}`) 313 | .set('Authorization', userToken) 314 | .send(taskUpdateRequestSuccess) 315 | .expect(200) 316 | .expect((res) => { 317 | res.body.data.task.created_at = 'fake_value'; 318 | res.body.data.task.updated_at = 'fake_value'; 319 | }) 320 | .expect({ 321 | message: 'task_update_by_id_success', 322 | data: { 323 | task: { 324 | notification_id: null, 325 | name: taskUpdateRequestSuccess.name, 326 | description: taskUpdateRequestSuccess.description, 327 | start_time: taskUpdateRequestSuccess.start_time, 328 | duration: taskUpdateRequestSuccess.duration, 329 | created_at: 'fake_value', 330 | updated_at: 'fake_value', 331 | user_id: user[0]._id.toString(), 332 | is_solved: taskUpdateRequestSuccess.is_solved, 333 | id: taskId, 334 | }, 335 | }, 336 | errors: null, 337 | }) 338 | .end(done); 339 | }); 340 | 341 | it('/tasks/{id} (DELETE) - should not delete task with invalid token', (done) => { 342 | return request(app.getHttpServer()) 343 | .delete(`/tasks/${taskId}`) 344 | .send() 345 | .expect(401) 346 | .expect({ 347 | message: 'token_decode_unauthorized', 348 | data: null, 349 | errors: null, 350 | }) 351 | .end(done); 352 | }); 353 | 354 | it('/tasks/{id} (DELETE) - should delete task with a valid token', (done) => { 355 | return request(app.getHttpServer()) 356 | .delete(`/tasks/${taskId}`) 357 | .set('Authorization', userToken) 358 | .send() 359 | .expect(200) 360 | .expect({ 361 | message: 'task_delete_by_id_success', 362 | data: null, 363 | errors: null, 364 | }) 365 | .end(done); 366 | }); 367 | }); 368 | -------------------------------------------------------------------------------- /gateway/test/timeout.ts: -------------------------------------------------------------------------------- 1 | jest.setTimeout(1000); 2 | -------------------------------------------------------------------------------- /gateway/test/user-confirm.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import * as mongoose from 'mongoose'; 4 | import { AppModule } from '../src/app.module'; 5 | import { userSignupRequestSuccess } from './mocks/user-signup-request-success.mock'; 6 | 7 | describe('Users Confirm Email (e2e)', () => { 8 | let app; 9 | let userToken: string; 10 | let userConfirmation: any[]; 11 | 12 | beforeAll(async () => { 13 | await mongoose.connect(process.env.MONGO_DSN, { useNewUrlParser: true }); 14 | await mongoose.connection.dropDatabase(); 15 | }); 16 | 17 | beforeEach(async () => { 18 | const moduleFixture: TestingModule = await Test.createTestingModule({ 19 | imports: [AppModule], 20 | }).compile(); 21 | 22 | app = moduleFixture.createNestApplication(); 23 | app.init(); 24 | }); 25 | 26 | it('/users/ (POST) - should create a valid user', (done) => { 27 | return request(app.getHttpServer()) 28 | .post('/users/') 29 | .send(userSignupRequestSuccess) 30 | .expect(201) 31 | .end(done); 32 | }); 33 | 34 | it('/users/login (POST) - should create a token for valid credentials', (done) => { 35 | return request(app.getHttpServer()) 36 | .post('/users/login') 37 | .send(userSignupRequestSuccess) 38 | .expect(201) 39 | .expect((res) => { 40 | userToken = res.body.data.token; 41 | }) 42 | .end(done); 43 | }); 44 | 45 | it('/users/ (GET) - should return an unconfirmed user before confirmation', (done) => { 46 | return request(app.getHttpServer()) 47 | .get('/users/') 48 | .set('Authorization', userToken) 49 | .send() 50 | .expect(200) 51 | .expect((res) => { 52 | res.body.data.user.id = 'fake_value'; 53 | res.body.data.user.email = 'fake_value'; 54 | }) 55 | .expect({ 56 | message: 'user_get_by_id_success', 57 | data: { 58 | user: { 59 | email: 'fake_value', 60 | is_confirmed: false, 61 | id: 'fake_value', 62 | }, 63 | }, 64 | errors: null, 65 | }) 66 | .end(done); 67 | }); 68 | 69 | it('/users/confirm/:link (GET) - should fail to confirm with no link', (done) => { 70 | return request(app.getHttpServer()) 71 | .get('/users/confirm/') 72 | .send() 73 | .expect(404) 74 | .end(done); 75 | }); 76 | 77 | it('/users/confirm/:link (GET) - should fail with invalid link', (done) => { 78 | return request(app.getHttpServer()) 79 | .get('/users/confirm/test') 80 | .send() 81 | .expect(404) 82 | .expect({ 83 | message: 'user_confirm_not_found', 84 | data: null, 85 | errors: null, 86 | }) 87 | .end(done); 88 | }); 89 | 90 | it('/users/confirm/:link (GET) - should succeed with a valid link', async () => { 91 | const user = await mongoose.connection 92 | .collection('users') 93 | .find({ 94 | email: userSignupRequestSuccess.email, 95 | }) 96 | .toArray(); 97 | 98 | userConfirmation = await mongoose.connection 99 | .collection('user_links') 100 | .find({ 101 | user_id: user[0]._id.toString(), 102 | }) 103 | .toArray(); 104 | 105 | return request(app.getHttpServer()) 106 | .get(`/users/confirm/${userConfirmation[0].link}`) 107 | .send() 108 | .expect(200) 109 | .expect({ 110 | message: 'user_confirm_success', 111 | errors: null, 112 | data: null, 113 | }); 114 | }); 115 | 116 | it('/users/ (GET) - should return a confirmed user after confirmation', (done) => { 117 | return request(app.getHttpServer()) 118 | .get('/users/') 119 | .set('Authorization', userToken) 120 | .send() 121 | .expect(200) 122 | .expect((res) => { 123 | res.body.data.user.id = 'fake_value'; 124 | res.body.data.user.email = 'fake_value'; 125 | }) 126 | .expect({ 127 | message: 'user_get_by_id_success', 128 | data: { 129 | user: { 130 | email: 'fake_value', 131 | is_confirmed: true, 132 | id: 'fake_value', 133 | }, 134 | }, 135 | errors: null, 136 | }) 137 | .end(done); 138 | }); 139 | 140 | it('/users/confirm/:link (GET) - should fail with a valid link second time', async () => { 141 | return request(app.getHttpServer()) 142 | .get(`/users/confirm/${userConfirmation[0].link}`) 143 | .send() 144 | .expect(404) 145 | .expect({ 146 | message: 'user_confirm_not_found', 147 | data: null, 148 | errors: null, 149 | }); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /gateway/test/user-sign-in.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import * as mongoose from 'mongoose'; 4 | import { AppModule } from './../src/app.module'; 5 | import { userSignupRequestSuccess } from './mocks/user-signup-request-success.mock'; 6 | import { 7 | userLoginRequestFailWrongPw, 8 | userLoginRequestFailWrongEmail, 9 | } from './mocks/user-login-request-fail.mock'; 10 | 11 | describe('Users Sign In (e2e)', () => { 12 | let app; 13 | 14 | beforeAll(async () => { 15 | await mongoose.connect(process.env.MONGO_DSN, { useNewUrlParser: true }); 16 | await mongoose.connection.dropDatabase(); 17 | }); 18 | 19 | beforeEach(async () => { 20 | const moduleFixture: TestingModule = await Test.createTestingModule({ 21 | imports: [AppModule], 22 | }).compile(); 23 | 24 | app = moduleFixture.createNestApplication(); 25 | await app.init(); 26 | }); 27 | 28 | it('/users/ (POST) - should create a valid user', (done) => { 29 | return request(app.getHttpServer()) 30 | .post('/users/') 31 | .send(userSignupRequestSuccess) 32 | .expect(201) 33 | .end(done); 34 | }); 35 | 36 | it('/users/login (POST) - should not create a token for invalid email', (done) => { 37 | return request(app.getHttpServer()) 38 | .post('/users/login') 39 | .send(userLoginRequestFailWrongEmail) 40 | .expect(401) 41 | .expect({ 42 | message: 'user_search_by_credentials_not_found', 43 | data: null, 44 | errors: null, 45 | }) 46 | .end(done); 47 | }); 48 | 49 | it('/users/login (POST) - should not create a token for invalid password', (done) => { 50 | return request(app.getHttpServer()) 51 | .post('/users/login') 52 | .send(userLoginRequestFailWrongPw) 53 | .expect(401) 54 | .expect({ 55 | message: 'user_search_by_credentials_not_match', 56 | data: null, 57 | errors: null, 58 | }) 59 | .end(done); 60 | }); 61 | 62 | it('/users/login (POST) - should not create a token for empty body', (done) => { 63 | return request(app.getHttpServer()) 64 | .post('/users/login') 65 | .send() 66 | .expect(401) 67 | .expect({ 68 | message: 'user_search_by_credentials_not_found', 69 | data: null, 70 | errors: null, 71 | }) 72 | .end(done); 73 | }); 74 | 75 | it('/users/login (POST) - should not create a token for string value in body', (done) => { 76 | return request(app.getHttpServer()) 77 | .post('/users/login') 78 | .send(userSignupRequestSuccess.email) 79 | .expect(401) 80 | .expect({ 81 | message: 'user_search_by_credentials_not_found', 82 | data: null, 83 | errors: null, 84 | }) 85 | .end(done); 86 | }); 87 | 88 | it('/users/login (POST) - should create a token for valid credentials', (done) => { 89 | return request(app.getHttpServer()) 90 | .post('/users/login') 91 | .send(userSignupRequestSuccess) 92 | .expect(201) 93 | .expect((res) => { 94 | res.body.data.token = 'fake_value'; 95 | }) 96 | .expect({ 97 | message: 'token_create_success', 98 | data: { 99 | token: 'fake_value', 100 | }, 101 | errors: null, 102 | }) 103 | .end(done); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /gateway/test/user-sign-out.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import * as mongoose from 'mongoose'; 4 | import { AppModule } from './../src/app.module'; 5 | import { userSignupRequestSuccess } from './mocks/user-signup-request-success.mock'; 6 | 7 | describe('Users Sign Out (e2e)', () => { 8 | let app; 9 | let userToken; 10 | 11 | beforeAll(async () => { 12 | await mongoose.connect(process.env.MONGO_DSN, { useNewUrlParser: true }); 13 | await mongoose.connection.dropDatabase(); 14 | }); 15 | 16 | beforeEach(async () => { 17 | const moduleFixture: TestingModule = await Test.createTestingModule({ 18 | imports: [AppModule], 19 | }).compile(); 20 | 21 | app = moduleFixture.createNestApplication(); 22 | await app.init(); 23 | }); 24 | 25 | it('/users/ (POST) - should create a valid user', (done) => { 26 | return request(app.getHttpServer()) 27 | .post('/users/') 28 | .send(userSignupRequestSuccess) 29 | .expect(201) 30 | .end(done); 31 | }); 32 | 33 | it('/users/login (POST) - should create a token for valid credentials', (done) => { 34 | return request(app.getHttpServer()) 35 | .post('/users/login') 36 | .send(userSignupRequestSuccess) 37 | .expect(201) 38 | .expect((res) => { 39 | userToken = res.body.data.token; 40 | }) 41 | .end(done); 42 | }); 43 | 44 | it('/users/ (GET) - should retrieve user by a valid token', (done) => { 45 | return request(app.getHttpServer()) 46 | .get('/users/') 47 | .set('Authorization', userToken) 48 | .send() 49 | .expect(200) 50 | .end(done); 51 | }); 52 | 53 | it('/users/logout (POST) - should destroy token for user', (done) => { 54 | return request(app.getHttpServer()) 55 | .put('/users/logout') 56 | .set('Authorization', userToken) 57 | .expect(200) 58 | .expect({ 59 | message: 'token_destroy_success', 60 | errors: null, 61 | data: null, 62 | }) 63 | .end(done); 64 | }); 65 | 66 | it('/users/ (GET) - should not retrieve user by a destroyed token', (done) => { 67 | return request(app.getHttpServer()) 68 | .get('/users/') 69 | .set('Authorization', userToken) 70 | .send() 71 | .expect(401) 72 | .expect({ 73 | message: 'token_decode_unauthorized', 74 | data: null, 75 | errors: null, 76 | }) 77 | .end(done); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /gateway/test/user-sign-up.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as mongoose from 'mongoose'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | import { userSignupRequestSuccess } from './mocks/user-signup-request-success.mock'; 6 | import { 7 | userSignupRequestFailNoPw, 8 | userSignupRequestFailShortPw, 9 | userSignupRequestFailInvalidEmail, 10 | } from './mocks/user-signup-request-fail.mock'; 11 | 12 | describe('Users Sign Up (e2e)', () => { 13 | let app; 14 | 15 | beforeAll(async () => { 16 | await mongoose.connect(process.env.MONGO_DSN, { useNewUrlParser: true }); 17 | await mongoose.connection.dropDatabase(); 18 | }); 19 | 20 | beforeEach(async () => { 21 | const moduleFixture: TestingModule = await Test.createTestingModule({ 22 | imports: [AppModule], 23 | }).compile(); 24 | 25 | app = moduleFixture.createNestApplication(); 26 | await app.init(); 27 | }); 28 | 29 | it('/users/ (POST) - should not create user without request body', (done) => { 30 | return request(app.getHttpServer()) 31 | .post('/users/') 32 | .send() 33 | .expect(412) 34 | .expect((res) => { 35 | res.body.errors.email.properties = 'fake_properties'; 36 | res.body.errors.password.properties = 'fake_properties'; 37 | }) 38 | .expect({ 39 | data: null, 40 | message: 'user_create_precondition_failed', 41 | errors: { 42 | password: { 43 | message: 'Password can not be empty', 44 | name: 'ValidatorError', 45 | properties: 'fake_properties', 46 | kind: 'required', 47 | path: 'password', 48 | }, 49 | email: { 50 | message: 'Email can not be empty', 51 | name: 'ValidatorError', 52 | properties: 'fake_properties', 53 | kind: 'required', 54 | path: 'email', 55 | }, 56 | }, 57 | }) 58 | .end(done); 59 | }); 60 | 61 | it('/users/ (POST) - should not create a user if request body is string', (done) => { 62 | return request(app.getHttpServer()) 63 | .post('/users/') 64 | .send('test') 65 | .expect((res) => { 66 | res.body.errors.email.properties = 'fake_properties'; 67 | res.body.errors.password.properties = 'fake_properties'; 68 | }) 69 | .expect({ 70 | data: null, 71 | message: 'user_create_precondition_failed', 72 | errors: { 73 | password: { 74 | message: 'Password can not be empty', 75 | name: 'ValidatorError', 76 | properties: 'fake_properties', 77 | kind: 'required', 78 | path: 'password', 79 | }, 80 | email: { 81 | message: 'Email can not be empty', 82 | name: 'ValidatorError', 83 | properties: 'fake_properties', 84 | kind: 'required', 85 | path: 'email', 86 | }, 87 | }, 88 | }) 89 | .end(done); 90 | }); 91 | 92 | it('/users/ (POST) - should not create user without password', (done) => { 93 | return request(app.getHttpServer()) 94 | .post('/users/') 95 | .send(userSignupRequestFailNoPw) 96 | .expect(412) 97 | .expect((res) => { 98 | res.body.errors.password.properties = 'fake_properties'; 99 | }) 100 | .expect({ 101 | data: null, 102 | message: 'user_create_precondition_failed', 103 | errors: { 104 | password: { 105 | message: 'Password can not be empty', 106 | name: 'ValidatorError', 107 | properties: 'fake_properties', 108 | kind: 'required', 109 | path: 'password', 110 | }, 111 | }, 112 | }) 113 | .end(done); 114 | }); 115 | 116 | it('/users/ (POST) - should not create user if password is short', (done) => { 117 | return request(app.getHttpServer()) 118 | .post('/users/') 119 | .send(userSignupRequestFailShortPw) 120 | .expect(412) 121 | .expect((res) => { 122 | res.body.errors.password.properties = 'fake_properties'; 123 | }) 124 | .expect({ 125 | data: null, 126 | message: 'user_create_precondition_failed', 127 | errors: { 128 | password: { 129 | message: 'Password should include at least 6 chars', 130 | name: 'ValidatorError', 131 | properties: 'fake_properties', 132 | kind: 'minlength', 133 | path: 'password', 134 | value: userSignupRequestFailShortPw.password, 135 | }, 136 | }, 137 | }) 138 | .end(done); 139 | }); 140 | 141 | it('/users/ (POST) - should not create user without email', (done) => { 142 | return request(app.getHttpServer()) 143 | .post('/users/') 144 | .send({ 145 | password: 'test111', 146 | }) 147 | .expect(412) 148 | .expect((res) => { 149 | res.body.errors.email.properties = 'fake_properties'; 150 | }) 151 | .expect({ 152 | data: null, 153 | message: 'user_create_precondition_failed', 154 | errors: { 155 | email: { 156 | message: 'Email can not be empty', 157 | name: 'ValidatorError', 158 | properties: 'fake_properties', 159 | kind: 'required', 160 | path: 'email', 161 | }, 162 | }, 163 | }) 164 | .end(done); 165 | }); 166 | 167 | it('/users/ (POST) - should not create user with invalid email', (done) => { 168 | return request(app.getHttpServer()) 169 | .post('/users/') 170 | .send(userSignupRequestFailInvalidEmail) 171 | .expect(412) 172 | .expect((res) => { 173 | res.body.errors.email.properties = 'fake_properties'; 174 | }) 175 | .expect({ 176 | data: null, 177 | message: 'user_create_precondition_failed', 178 | errors: { 179 | email: { 180 | message: 'Email should be valid', 181 | name: 'ValidatorError', 182 | properties: 'fake_properties', 183 | kind: 'regexp', 184 | path: 'email', 185 | value: userSignupRequestFailInvalidEmail.email, 186 | }, 187 | }, 188 | }) 189 | .end(done); 190 | }); 191 | 192 | it('/users/ (POST) - should create a valid user', (done) => { 193 | return request(app.getHttpServer()) 194 | .post('/users/') 195 | .send(userSignupRequestSuccess) 196 | .expect(201) 197 | .expect((res) => { 198 | res.body.data.user.id = 'fake_value'; 199 | res.body.data.token = 'fake_value'; 200 | }) 201 | .expect({ 202 | message: 'user_create_success', 203 | data: { 204 | user: { 205 | email: userSignupRequestSuccess.email, 206 | is_confirmed: false, 207 | id: 'fake_value', 208 | }, 209 | token: 'fake_value', 210 | }, 211 | errors: null, 212 | }) 213 | .end(done); 214 | }); 215 | 216 | it('/users/ (POST) - should not create user with existing email', (done) => { 217 | return request(app.getHttpServer()) 218 | .post('/users/') 219 | .send(userSignupRequestSuccess) 220 | .expect(409) 221 | .expect({ 222 | message: 'user_create_conflict', 223 | data: null, 224 | errors: { 225 | email: { 226 | message: 'Email already exists', 227 | path: 'email', 228 | }, 229 | }, 230 | }) 231 | .end(done); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /gateway/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true, 13 | "skipLibCheck": true 14 | }, 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /gateway/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 120], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false, 16 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] 17 | }, 18 | "rulesDirectory": [] 19 | } 20 | -------------------------------------------------------------------------------- /mailer/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /mailer/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /mailer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /mailer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0-alpine 2 | RUN npm install -g npm@6.14.7 3 | RUN mkdir -p /var/www/mailer 4 | WORKDIR /var/www/mailer 5 | ADD . /var/www/mailer 6 | RUN npm install 7 | CMD npm run build && npm run start:prod 8 | -------------------------------------------------------------------------------- /mailer/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /mailer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailer", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rimraf dist && tsc -p tsconfig.build.json", 9 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env\"", 10 | "start:test": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env.test\"", 11 | "start:prod": "node dist/main.js", 12 | "lint": "eslint \"src/**/*.ts\" --fix" 13 | }, 14 | "dependencies": { 15 | "@nest-modules/mailer": "1.3.22", 16 | "@nestjs/common": "8.0.0", 17 | "@nestjs/core": "8.0.0", 18 | "@nestjs/microservices": "8.0.0", 19 | "@nestjs/platform-express": "8.0.0", 20 | "nodemailer": "^6.4.11", 21 | "reflect-metadata": "0.1.13", 22 | "rimraf": "3.0.2", 23 | "rxjs": "7.3.0" 24 | }, 25 | "devDependencies": { 26 | "@types/express": "4.17.8", 27 | "@types/node": "14.0.27", 28 | "@types/nodemailer": "^6.4.0", 29 | "dotenv": "8.2.0", 30 | "ts-node": "9.0.0", 31 | "tsc-watch": "4.2.9", 32 | "tsconfig-paths": "3.9.0", 33 | "typescript": "4.0.5", 34 | "prettier": "2.1.2", 35 | "eslint-config-prettier": "7.0.0", 36 | "eslint-plugin-prettier": "^3.1.4", 37 | "@typescript-eslint/eslint-plugin": "4.6.1", 38 | "@typescript-eslint/parser": "4.6.1", 39 | "eslint": "7.12.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mailer/src/interfaces/email-data.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IEmailData { 2 | to: string; 3 | subject: string; 4 | text: string; 5 | html?: string; 6 | } 7 | -------------------------------------------------------------------------------- /mailer/src/interfaces/mail-send-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IMailSendResponse { 2 | status: number; 3 | message: string; 4 | } 5 | -------------------------------------------------------------------------------- /mailer/src/mailer.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpStatus } from '@nestjs/common'; 2 | import { MessagePattern } from '@nestjs/microservices'; 3 | import { MailerService } from '@nest-modules/mailer'; 4 | 5 | import { ConfigService } from './services/config/config.service'; 6 | import { IEmailData } from './interfaces/email-data.interface'; 7 | import { IMailSendResponse } from './interfaces/mail-send-response.interface'; 8 | 9 | @Controller() 10 | export class MailerController { 11 | constructor( 12 | private readonly mailerService: MailerService, 13 | private readonly configService: ConfigService, 14 | ) {} 15 | 16 | @MessagePattern('mail_send') 17 | mailSend(data: IEmailData): IMailSendResponse { 18 | if (!this.configService.get('emailsDisabled')) { 19 | this.mailerService.sendMail(data); 20 | } 21 | return { 22 | status: HttpStatus.ACCEPTED, 23 | message: 'mail_send_success', 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mailer/src/mailer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MailerModule } from '@nest-modules/mailer'; 3 | import { MailerController } from './mailer.controller'; 4 | import { MailerConfigService } from './services/config/mailer-config.service'; 5 | import { ConfigService } from './services/config/config.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | MailerModule.forRootAsync({ 10 | useClass: MailerConfigService, 11 | }), 12 | ], 13 | providers: [ConfigService], 14 | controllers: [MailerController], 15 | }) 16 | export class AppMailerModule {} 17 | -------------------------------------------------------------------------------- /mailer/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { Transport, TcpOptions } from '@nestjs/microservices'; 3 | 4 | import { AppMailerModule } from './mailer.module'; 5 | import { ConfigService } from './services/config/config.service'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.createMicroservice(AppMailerModule, { 9 | transport: Transport.TCP, 10 | options: { 11 | host: '0.0.0.0', 12 | port: new ConfigService().get('port'), 13 | }, 14 | } as TcpOptions); 15 | await app.listenAsync(); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /mailer/src/services/config/config.service.ts: -------------------------------------------------------------------------------- 1 | export class ConfigService { 2 | private readonly envConfig: { [key: string]: any } = null; 3 | 4 | constructor() { 5 | this.envConfig = { 6 | port: process.env.MAILER_SERVICE_PORT, 7 | emailsDisabled: process.env.MAILER_DISABLED, 8 | }; 9 | } 10 | 11 | get(key: string): any { 12 | return this.envConfig[key]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mailer/src/services/config/mailer-config.service.ts: -------------------------------------------------------------------------------- 1 | import { MailerOptionsFactory, MailerOptions } from '@nest-modules/mailer'; 2 | 3 | export class MailerConfigService implements MailerOptionsFactory { 4 | createMailerOptions(): MailerOptions { 5 | return { 6 | transport: process.env.MAILER_DSN, 7 | defaults: { 8 | from: process.env.MAILER_FROM, 9 | }, 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mailer/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /mailer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /mailer/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 120], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false, 16 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}] 17 | }, 18 | "rulesDirectory": [] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smoothday-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "npm --prefix task run lint && npm --prefix user run lint && npm --prefix token run lint && npm --prefix gateway run lint && npm --prefix mailer run lint && npm --prefix permission run lint" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Denrox/nestjs-microservices-example.git" 12 | }, 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /permission/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /permission/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /permission/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /permission/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0-alpine 2 | RUN npm install -g npm@6.14.7 3 | RUN mkdir -p /var/www/permission 4 | WORKDIR /var/www/permission 5 | ADD . /var/www/permission 6 | RUN npm install 7 | CMD npm run build && npm run start:prod 8 | -------------------------------------------------------------------------------- /permission/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /permission/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "permission", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rimraf dist && tsc -p tsconfig.build.json", 9 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env\"", 10 | "start:test": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env.test\"", 11 | "start:prod": "node dist/main.js", 12 | "lint": "eslint \"src/**/*.ts\" --fix" 13 | }, 14 | "dependencies": { 15 | "@nestjs/common": "8.0.0", 16 | "@nestjs/core": "8.0.0", 17 | "@nestjs/microservices": "8.0.0", 18 | "@nestjs/platform-express": "8.0.0", 19 | "reflect-metadata": "0.1.13", 20 | "rimraf": "3.0.2", 21 | "rxjs": "7.3.0" 22 | }, 23 | "devDependencies": { 24 | "@types/express": "4.17.8", 25 | "@types/node": "14.0.27", 26 | "dotenv": "8.2.0", 27 | "ts-node": "9.0.0", 28 | "tsc-watch": "4.2.9", 29 | "tsconfig-paths": "3.9.0", 30 | "typescript": "4.0.5", 31 | "prettier": "2.1.2", 32 | "eslint-config-prettier": "7.0.0", 33 | "eslint-plugin-prettier": "^3.1.4", 34 | "@typescript-eslint/eslint-plugin": "4.6.1", 35 | "@typescript-eslint/parser": "4.6.1", 36 | "eslint": "7.12.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /permission/src/constants/permissions.ts: -------------------------------------------------------------------------------- 1 | export const permissions = [ 2 | 'user_get_by_id', 3 | 'user_confirm', 4 | 'task_search_by_user_id', 5 | 'task_create', 6 | 'task_delete_by_id', 7 | 'task_update_by_id', 8 | ]; 9 | -------------------------------------------------------------------------------- /permission/src/interfaces/permission-check-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IPermissionCheckResponse { 2 | status: number; 3 | message: string; 4 | errors: null; 5 | } 6 | -------------------------------------------------------------------------------- /permission/src/interfaces/permission-strategy.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './user.interface'; 2 | 3 | export interface IPermissionStrategy { 4 | getAllowedPermissions: (user: IUser, permissions: string[]) => string[]; 5 | } 6 | -------------------------------------------------------------------------------- /permission/src/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id?: string; 3 | email: string; 4 | is_confirmed: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /permission/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { Transport, TcpOptions } from '@nestjs/microservices'; 3 | 4 | import { PermissionModule } from './permission.module'; 5 | import { ConfigService } from './services/config/config.service'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.createMicroservice(PermissionModule, { 9 | transport: Transport.TCP, 10 | options: { 11 | host: '0.0.0.0', 12 | port: new ConfigService().get('port'), 13 | }, 14 | } as TcpOptions); 15 | await app.listenAsync(); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /permission/src/permission.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpStatus } from '@nestjs/common'; 2 | import { MessagePattern } from '@nestjs/microservices'; 3 | import { ConfirmedStrategyService } from './services/confirmed-strategy.service'; 4 | import { permissions } from './constants/permissions'; 5 | import { IPermissionCheckResponse } from './interfaces/permission-check-response.interface'; 6 | import { IUser } from './interfaces/user.interface'; 7 | 8 | @Controller() 9 | export class PermissionController { 10 | constructor(private confirmedStrategy: ConfirmedStrategyService) {} 11 | 12 | @MessagePattern('permission_check') 13 | public permissionCheck(permissionParams: { 14 | user: IUser; 15 | permission: string; 16 | }): IPermissionCheckResponse { 17 | let result: IPermissionCheckResponse; 18 | 19 | if (!permissionParams || !permissionParams.user) { 20 | result = { 21 | status: HttpStatus.BAD_REQUEST, 22 | message: 'permission_check_bad_request', 23 | errors: null, 24 | }; 25 | } else { 26 | const allowedPermissions = this.confirmedStrategy.getAllowedPermissions( 27 | permissionParams.user, 28 | permissions, 29 | ); 30 | const isAllowed = allowedPermissions.includes( 31 | permissionParams.permission, 32 | ); 33 | 34 | result = { 35 | status: isAllowed ? HttpStatus.OK : HttpStatus.FORBIDDEN, 36 | message: isAllowed 37 | ? 'permission_check_success' 38 | : 'permission_check_forbidden', 39 | errors: null, 40 | }; 41 | } 42 | 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /permission/src/permission.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './services/config/config.service'; 3 | import { ConfirmedStrategyService } from './services/confirmed-strategy.service'; 4 | import { PermissionController } from './permission.controller'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [PermissionController], 9 | providers: [ConfigService, ConfirmedStrategyService], 10 | }) 11 | export class PermissionModule {} 12 | -------------------------------------------------------------------------------- /permission/src/services/config/config.service.ts: -------------------------------------------------------------------------------- 1 | export class ConfigService { 2 | private readonly envConfig: { [key: string]: any } = null; 3 | 4 | constructor() { 5 | this.envConfig = { 6 | port: process.env.PERMISSION_SERVICE_PORT, 7 | }; 8 | } 9 | 10 | get(key: string): any { 11 | return this.envConfig[key]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /permission/src/services/confirmed-strategy.service.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionStrategy } from '../interfaces/permission-strategy.interface'; 2 | import { IUser } from '../interfaces/user.interface'; 3 | 4 | export class ConfirmedStrategyService implements IPermissionStrategy { 5 | public getAllowedPermissions(user: IUser, permissions: string[]): string[] { 6 | const forbiddenPermissions = [ 7 | 'task_search_by_user_id', 8 | 'task_create', 9 | 'task_delete_by_id', 10 | 'task_update_by_id', 11 | ]; 12 | return user.is_confirmed 13 | ? permissions 14 | : permissions.filter((permission: string) => { 15 | return !forbiddenPermissions.includes(permission); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /permission/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /permission/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /task/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /task/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /task/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /task/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0-alpine 2 | RUN npm install -g npm@6.14.7 3 | RUN mkdir -p /var/www/task 4 | WORKDIR /var/www/task 5 | ADD . /var/www/task 6 | RUN npm install 7 | CMD npm run build && npm run start:prod 8 | -------------------------------------------------------------------------------- /task/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /task/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rimraf dist && tsc -p tsconfig.build.json", 9 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env\"", 10 | "start:test": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env.test\"", 11 | "start:prod": "node dist/main.js", 12 | "lint": "eslint \"src/**/*.ts\" --fix" 13 | }, 14 | "dependencies": { 15 | "@nestjs/common": "8.0.0", 16 | "@nestjs/core": "8.0.0", 17 | "@nestjs/microservices": "8.0.0", 18 | "@nestjs/mongoose": "7.2.4", 19 | "@nestjs/platform-express": "8.0.0", 20 | "mongoose": "5.11.15", 21 | "reflect-metadata": "0.1.13", 22 | "rimraf": "3.0.2", 23 | "rxjs": "7.3.0" 24 | }, 25 | "devDependencies": { 26 | "@types/express": "4.17.8", 27 | "@types/node": "14.0.27", 28 | "dotenv": "8.2.0", 29 | "ts-node": "9.0.0", 30 | "tsc-watch": "4.2.9", 31 | "tsconfig-paths": "3.9.0", 32 | "typescript": "4.0.5", 33 | "prettier": "2.1.2", 34 | "eslint-config-prettier": "7.0.0", 35 | "eslint-plugin-prettier": "^3.1.4", 36 | "@typescript-eslint/eslint-plugin": "4.6.1", 37 | "@typescript-eslint/parser": "4.6.1", 38 | "eslint": "7.12.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /task/src/interfaces/task-create-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { ITask } from './task.interface'; 2 | 3 | export interface ITaskCreateResponse { 4 | status: number; 5 | message: string; 6 | task: ITask | null; 7 | errors: { [key: string]: any } | null; 8 | } 9 | -------------------------------------------------------------------------------- /task/src/interfaces/task-delete-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITaskDeleteResponse { 2 | status: number; 3 | message: string; 4 | errors: { [key: string]: any } | null; 5 | } 6 | -------------------------------------------------------------------------------- /task/src/interfaces/task-search-by-user-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { ITask } from './task.interface'; 2 | 3 | export interface ITaskSearchByUserResponse { 4 | status: number; 5 | message: string; 6 | tasks: ITask[]; 7 | } 8 | -------------------------------------------------------------------------------- /task/src/interfaces/task-update-by-id-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { ITask } from './task.interface'; 2 | 3 | export interface ITaskUpdateByIdResponse { 4 | status: number; 5 | message: string; 6 | task: ITask | null; 7 | errors: { [key: string]: any } | null; 8 | } 9 | -------------------------------------------------------------------------------- /task/src/interfaces/task-update-params.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITaskUpdateParams { 2 | name: string; 3 | description: string; 4 | start_time: number; 5 | duration: number; 6 | is_solved: boolean; 7 | notification_id: number; 8 | } 9 | -------------------------------------------------------------------------------- /task/src/interfaces/task.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export interface ITask extends Document { 4 | name: string; 5 | description: string; 6 | user_id: string; 7 | start_time: number; 8 | duration: number; 9 | is_solved: boolean; 10 | notification_id: number; 11 | created_at: number; 12 | } 13 | -------------------------------------------------------------------------------- /task/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { TaskModule } from './task.module'; 3 | import { Transport, TcpOptions } from '@nestjs/microservices'; 4 | 5 | import { ConfigService } from './services/config/config.service'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.createMicroservice(TaskModule, { 9 | transport: Transport.TCP, 10 | options: { 11 | host: '0.0.0.0', 12 | port: new ConfigService().get('port'), 13 | }, 14 | } as TcpOptions); 15 | await app.listenAsync(); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /task/src/schemas/task.schema.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import { ITask } from '../interfaces/task.interface'; 3 | 4 | function transformValue(doc, ret: { [key: string]: any }) { 5 | delete ret._id; 6 | } 7 | 8 | export const TaskSchema = new mongoose.Schema( 9 | { 10 | name: { 11 | type: String, 12 | required: [true, 'Name can not be empty'], 13 | }, 14 | description: String, 15 | user_id: { 16 | type: String, 17 | required: [true, 'User can not be empty'], 18 | }, 19 | start_time: { 20 | type: Number, 21 | required: [true, 'Start time can not be empty'], 22 | }, 23 | duration: { 24 | type: Number, 25 | required: [true, 'Duration can not be empty'], 26 | }, 27 | is_solved: { 28 | type: Boolean, 29 | required: [true, 'Solved flag can not be empty'], 30 | }, 31 | notification_id: { 32 | type: Number, 33 | required: false, 34 | default: null, 35 | }, 36 | }, 37 | { 38 | timestamps: { 39 | createdAt: 'created_at', 40 | updatedAt: 'updated_at', 41 | }, 42 | toObject: { 43 | virtuals: true, 44 | versionKey: false, 45 | transform: transformValue, 46 | }, 47 | toJSON: { 48 | virtuals: true, 49 | versionKey: false, 50 | transform: transformValue, 51 | }, 52 | }, 53 | ); 54 | 55 | TaskSchema.pre('validate', function (next) { 56 | const self = this as ITask; 57 | 58 | if (this.isModified('user_id') && self.created_at) { 59 | this.invalidate('user_id', 'The field value can not be updated'); 60 | } 61 | next(); 62 | }); 63 | -------------------------------------------------------------------------------- /task/src/services/config/config.service.ts: -------------------------------------------------------------------------------- 1 | export class ConfigService { 2 | private readonly envConfig: { [key: string]: any } = null; 3 | 4 | constructor() { 5 | this.envConfig = { 6 | port: process.env.TASK_SERVICE_PORT, 7 | }; 8 | } 9 | 10 | get(key: string): any { 11 | return this.envConfig[key]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /task/src/services/config/mongo-config.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MongooseOptionsFactory, 3 | MongooseModuleOptions, 4 | } from '@nestjs/mongoose'; 5 | 6 | export class MongoConfigService implements MongooseOptionsFactory { 7 | createMongooseOptions(): MongooseModuleOptions { 8 | return { 9 | uri: process.env.MONGO_DSN, 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /task/src/services/task.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | 5 | import { ITask } from '../interfaces/task.interface'; 6 | import { ITaskUpdateParams } from '../interfaces/task-update-params.interface'; 7 | 8 | @Injectable() 9 | export class TaskService { 10 | constructor(@InjectModel('Task') private readonly taskModel: Model) {} 11 | 12 | public async getTasksByUserId(userId: string): Promise { 13 | return this.taskModel.find({ user_id: userId }).exec(); 14 | } 15 | 16 | public async createTask(taskBody: ITask): Promise { 17 | const taskModel = new this.taskModel(taskBody); 18 | return await taskModel.save(); 19 | } 20 | 21 | public async findTaskById(id: string) { 22 | return await this.taskModel.findById(id); 23 | } 24 | 25 | public async removeTaskById(id: string) { 26 | return await this.taskModel.findOneAndDelete({ _id: id }); 27 | } 28 | 29 | public async updateTaskById( 30 | id: string, 31 | params: ITaskUpdateParams, 32 | ): Promise { 33 | return await this.taskModel.updateOne({ _id: id }, params); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /task/src/task.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpStatus } from '@nestjs/common'; 2 | import { MessagePattern } from '@nestjs/microservices'; 3 | 4 | import { TaskService } from './services/task.service'; 5 | import { ITask } from './interfaces/task.interface'; 6 | import { ITaskUpdateParams } from './interfaces/task-update-params.interface'; 7 | import { ITaskSearchByUserResponse } from './interfaces/task-search-by-user-response.interface'; 8 | import { ITaskDeleteResponse } from './interfaces/task-delete-response.interface'; 9 | import { ITaskCreateResponse } from './interfaces/task-create-response.interface'; 10 | import { ITaskUpdateByIdResponse } from './interfaces/task-update-by-id-response.interface'; 11 | 12 | @Controller() 13 | export class TaskController { 14 | constructor(private readonly taskService: TaskService) {} 15 | 16 | @MessagePattern('task_search_by_user_id') 17 | public async taskSearchByUserId( 18 | userId: string, 19 | ): Promise { 20 | let result: ITaskSearchByUserResponse; 21 | 22 | if (userId) { 23 | const tasks = await this.taskService.getTasksByUserId(userId); 24 | result = { 25 | status: HttpStatus.OK, 26 | message: 'task_search_by_user_id_success', 27 | tasks, 28 | }; 29 | } else { 30 | result = { 31 | status: HttpStatus.BAD_REQUEST, 32 | message: 'task_search_by_user_id_bad_request', 33 | tasks: null, 34 | }; 35 | } 36 | 37 | return result; 38 | } 39 | 40 | @MessagePattern('task_update_by_id') 41 | public async taskUpdateById(params: { 42 | task: ITaskUpdateParams; 43 | id: string; 44 | userId: string; 45 | }): Promise { 46 | let result: ITaskUpdateByIdResponse; 47 | if (params.id) { 48 | try { 49 | const task = await this.taskService.findTaskById(params.id); 50 | if (task) { 51 | if (task.user_id === params.userId) { 52 | const updatedTask = Object.assign(task, params.task); 53 | await updatedTask.save(); 54 | result = { 55 | status: HttpStatus.OK, 56 | message: 'task_update_by_id_success', 57 | task: updatedTask, 58 | errors: null, 59 | }; 60 | } else { 61 | result = { 62 | status: HttpStatus.FORBIDDEN, 63 | message: 'task_update_by_id_forbidden', 64 | task: null, 65 | errors: null, 66 | }; 67 | } 68 | } else { 69 | result = { 70 | status: HttpStatus.NOT_FOUND, 71 | message: 'task_update_by_id_not_found', 72 | task: null, 73 | errors: null, 74 | }; 75 | } 76 | } catch (e) { 77 | result = { 78 | status: HttpStatus.PRECONDITION_FAILED, 79 | message: 'task_update_by_id_precondition_failed', 80 | task: null, 81 | errors: e.errors, 82 | }; 83 | } 84 | } else { 85 | result = { 86 | status: HttpStatus.BAD_REQUEST, 87 | message: 'task_update_by_id_bad_request', 88 | task: null, 89 | errors: null, 90 | }; 91 | } 92 | 93 | return result; 94 | } 95 | 96 | @MessagePattern('task_create') 97 | public async taskCreate(taskBody: ITask): Promise { 98 | let result: ITaskCreateResponse; 99 | 100 | if (taskBody) { 101 | try { 102 | taskBody.notification_id = null; 103 | taskBody.is_solved = false; 104 | const task = await this.taskService.createTask(taskBody); 105 | result = { 106 | status: HttpStatus.CREATED, 107 | message: 'task_create_success', 108 | task, 109 | errors: null, 110 | }; 111 | } catch (e) { 112 | result = { 113 | status: HttpStatus.PRECONDITION_FAILED, 114 | message: 'task_create_precondition_failed', 115 | task: null, 116 | errors: e.errors, 117 | }; 118 | } 119 | } else { 120 | result = { 121 | status: HttpStatus.BAD_REQUEST, 122 | message: 'task_create_bad_request', 123 | task: null, 124 | errors: null, 125 | }; 126 | } 127 | 128 | return result; 129 | } 130 | 131 | @MessagePattern('task_delete_by_id') 132 | public async taskDeleteForUser(params: { 133 | userId: string; 134 | id: string; 135 | }): Promise { 136 | let result: ITaskDeleteResponse; 137 | 138 | if (params && params.userId && params.id) { 139 | try { 140 | const task = await this.taskService.findTaskById(params.id); 141 | 142 | if (task) { 143 | if (task.user_id === params.userId) { 144 | await this.taskService.removeTaskById(params.id); 145 | result = { 146 | status: HttpStatus.OK, 147 | message: 'task_delete_by_id_success', 148 | errors: null, 149 | }; 150 | } else { 151 | result = { 152 | status: HttpStatus.FORBIDDEN, 153 | message: 'task_delete_by_id_forbidden', 154 | errors: null, 155 | }; 156 | } 157 | } else { 158 | result = { 159 | status: HttpStatus.NOT_FOUND, 160 | message: 'task_delete_by_id_not_found', 161 | errors: null, 162 | }; 163 | } 164 | } catch (e) { 165 | result = { 166 | status: HttpStatus.FORBIDDEN, 167 | message: 'task_delete_by_id_forbidden', 168 | errors: null, 169 | }; 170 | } 171 | } else { 172 | result = { 173 | status: HttpStatus.BAD_REQUEST, 174 | message: 'task_delete_by_id_bad_request', 175 | errors: null, 176 | }; 177 | } 178 | 179 | return result; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /task/src/task.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | import { MongoConfigService } from './services/config/mongo-config.service'; 5 | import { TaskController } from './task.controller'; 6 | import { TaskService } from './services/task.service'; 7 | import { TaskSchema } from './schemas/task.schema'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forRootAsync({ 12 | useClass: MongoConfigService, 13 | }), 14 | MongooseModule.forFeature([ 15 | { 16 | name: 'Task', 17 | schema: TaskSchema, 18 | }, 19 | ]), 20 | ], 21 | controllers: [TaskController], 22 | providers: [TaskService], 23 | }) 24 | export class TaskModule {} 25 | -------------------------------------------------------------------------------- /task/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /task/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /token/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /token/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /token/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /token/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0-alpine 2 | RUN npm install -g npm@6.14.7 3 | RUN mkdir -p /var/www/token 4 | WORKDIR /var/www/token 5 | ADD . /var/www/token 6 | RUN npm install 7 | CMD npm run build && npm run start:prod 8 | -------------------------------------------------------------------------------- /token/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /token/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rimraf dist && tsc -p tsconfig.build.json", 9 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env\"", 10 | "start:test": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env.test\"", 11 | "start:prod": "node dist/main.js", 12 | "lint": "eslint \"src/**/*.ts\" --fix" 13 | }, 14 | "dependencies": { 15 | "@nestjs/common": "8.0.0", 16 | "@nestjs/core": "8.0.0", 17 | "@nestjs/microservices": "8.0.0", 18 | "@nestjs/mongoose": "7.2.4", 19 | "@nestjs/platform-express": "8.0.0", 20 | "@nestjs/jwt": "8.0.0", 21 | "mongoose": "5.11.15", 22 | "reflect-metadata": "0.1.13", 23 | "rimraf": "3.0.2", 24 | "rxjs": "7.3.0" 25 | }, 26 | "devDependencies": { 27 | "@types/express": "4.17.8", 28 | "@types/node": "14.0.27", 29 | "dotenv": "8.2.0", 30 | "ts-node": "9.0.0", 31 | "tsc-watch": "4.2.9", 32 | "tsconfig-paths": "3.9.0", 33 | "typescript": "4.0.5", 34 | "prettier": "2.1.2", 35 | "eslint-config-prettier": "7.0.0", 36 | "eslint-plugin-prettier": "^3.1.4", 37 | "@typescript-eslint/eslint-plugin": "4.6.1", 38 | "@typescript-eslint/parser": "4.6.1", 39 | "eslint": "7.12.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /token/src/interfaces/token-data-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITokenDataResponse { 2 | status: number; 3 | message: string; 4 | data: { userId: string } | null; 5 | } 6 | -------------------------------------------------------------------------------- /token/src/interfaces/token-destroy-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITokenDestroyResponse { 2 | status: number; 3 | message: string; 4 | errors: { [key: string]: any } | null; 5 | } 6 | -------------------------------------------------------------------------------- /token/src/interfaces/token-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITokenResponse { 2 | status: number; 3 | token: string | null; 4 | message: string; 5 | } 6 | -------------------------------------------------------------------------------- /token/src/interfaces/token.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export interface IToken extends Document { 4 | user_id: string; 5 | token: string; 6 | } 7 | -------------------------------------------------------------------------------- /token/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { Transport, TcpOptions } from '@nestjs/microservices'; 3 | 4 | import { TokenModule } from './token.module'; 5 | import { ConfigService } from './services/config/config.service'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.createMicroservice(TokenModule, { 9 | transport: Transport.TCP, 10 | options: { 11 | host: '0.0.0.0', 12 | port: new ConfigService().get('port'), 13 | }, 14 | } as TcpOptions); 15 | await app.listenAsync(); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /token/src/schemas/token.schema.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | 3 | function transformValue(doc, ret: { [key: string]: any }) { 4 | delete ret._id; 5 | } 6 | 7 | export const TokenSchema = new mongoose.Schema( 8 | { 9 | user_id: { 10 | type: String, 11 | required: [true, 'User can not be empty'], 12 | }, 13 | token: { 14 | type: String, 15 | required: [true, 'Token can not be empty'], 16 | }, 17 | }, 18 | { 19 | toObject: { 20 | virtuals: true, 21 | versionKey: false, 22 | transform: transformValue, 23 | }, 24 | toJSON: { 25 | virtuals: true, 26 | versionKey: false, 27 | transform: transformValue, 28 | }, 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /token/src/services/config/config.service.ts: -------------------------------------------------------------------------------- 1 | export class ConfigService { 2 | private readonly envConfig: { [key: string]: any } = null; 3 | 4 | constructor() { 5 | this.envConfig = { 6 | port: process.env.TOKEN_SERVICE_PORT, 7 | }; 8 | } 9 | 10 | get(key: string): any { 11 | return this.envConfig[key]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /token/src/services/config/jwt-config.service.ts: -------------------------------------------------------------------------------- 1 | import { JwtOptionsFactory, JwtModuleOptions } from '@nestjs/jwt'; 2 | 3 | export class JwtConfigService implements JwtOptionsFactory { 4 | createJwtOptions(): JwtModuleOptions { 5 | return { 6 | secret: ', 12 | ) {} 13 | 14 | public createToken(userId: string): Promise { 15 | const token = this.jwtService.sign( 16 | { 17 | userId, 18 | }, 19 | { 20 | expiresIn: 30 * 24 * 60 * 60, 21 | }, 22 | ); 23 | 24 | return new this.tokenModel({ 25 | user_id: userId, 26 | token, 27 | }).save(); 28 | } 29 | 30 | public deleteTokenForUserId(userId: string): Query { 31 | return this.tokenModel.remove({ 32 | user_id: userId, 33 | }); 34 | } 35 | 36 | public async decodeToken(token: string) { 37 | const tokenModel = await this.tokenModel.find({ 38 | token, 39 | }); 40 | let result = null; 41 | 42 | if (tokenModel && tokenModel[0]) { 43 | try { 44 | const tokenData = this.jwtService.decode(tokenModel[0].token) as { 45 | exp: number; 46 | userId: any; 47 | }; 48 | if (!tokenData || tokenData.exp <= Math.floor(+new Date() / 1000)) { 49 | result = null; 50 | } else { 51 | result = { 52 | userId: tokenData.userId, 53 | }; 54 | } 55 | } catch (e) { 56 | result = null; 57 | } 58 | } 59 | return result; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /token/src/token.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpStatus } from '@nestjs/common'; 2 | import { MessagePattern } from '@nestjs/microservices'; 3 | import { TokenService } from './services/token.service'; 4 | import { ITokenResponse } from './interfaces/token-response.interface'; 5 | import { ITokenDataResponse } from './interfaces/token-data-response.interface'; 6 | import { ITokenDestroyResponse } from './interfaces/token-destroy-response.interface'; 7 | 8 | @Controller('token') 9 | export class TokenController { 10 | constructor(private readonly tokenService: TokenService) {} 11 | 12 | @MessagePattern('token_create') 13 | public async createToken(data: { userId: string }): Promise { 14 | let result: ITokenResponse; 15 | if (data && data.userId) { 16 | try { 17 | const createResult = await this.tokenService.createToken(data.userId); 18 | result = { 19 | status: HttpStatus.CREATED, 20 | message: 'token_create_success', 21 | token: createResult.token, 22 | }; 23 | } catch (e) { 24 | result = { 25 | status: HttpStatus.BAD_REQUEST, 26 | message: 'token_create_bad_request', 27 | token: null, 28 | }; 29 | } 30 | } else { 31 | result = { 32 | status: HttpStatus.BAD_REQUEST, 33 | message: 'token_create_bad_request', 34 | token: null, 35 | }; 36 | } 37 | 38 | return result; 39 | } 40 | 41 | @MessagePattern('token_destroy') 42 | public async destroyToken(data: { 43 | userId: string; 44 | }): Promise { 45 | return { 46 | status: data && data.userId ? HttpStatus.OK : HttpStatus.BAD_REQUEST, 47 | message: 48 | data && data.userId 49 | ? (await this.tokenService.deleteTokenForUserId(data.userId)) && 50 | 'token_destroy_success' 51 | : 'token_destroy_bad_request', 52 | errors: null, 53 | }; 54 | } 55 | 56 | @MessagePattern('token_decode') 57 | public async decodeToken(data: { 58 | token: string; 59 | }): Promise { 60 | const tokenData = await this.tokenService.decodeToken(data.token); 61 | return { 62 | status: tokenData ? HttpStatus.OK : HttpStatus.UNAUTHORIZED, 63 | message: tokenData ? 'token_decode_success' : 'token_decode_unauthorized', 64 | data: tokenData, 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /token/src/token.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | import { TokenController } from './token.controller'; 5 | import { TokenService } from './services/token.service'; 6 | import { JwtConfigService } from './services/config/jwt-config.service'; 7 | import { MongoConfigService } from './services/config/mongo-config.service'; 8 | import { TokenSchema } from './schemas/token.schema'; 9 | 10 | @Module({ 11 | imports: [ 12 | JwtModule.registerAsync({ 13 | useClass: JwtConfigService, 14 | }), 15 | MongooseModule.forRootAsync({ 16 | useClass: MongoConfigService, 17 | }), 18 | MongooseModule.forFeature([ 19 | { 20 | name: 'Token', 21 | schema: TokenSchema, 22 | }, 23 | ]), 24 | ], 25 | controllers: [TokenController], 26 | providers: [TokenService], 27 | }) 28 | export class TokenModule {} 29 | -------------------------------------------------------------------------------- /token/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /token/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /user/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | -------------------------------------------------------------------------------- /user/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier/@typescript-eslint', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /user/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /user/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0-alpine 2 | RUN npm install -g npm@6.14.7 3 | RUN mkdir -p /var/www/user 4 | WORKDIR /var/www/user 5 | ADD . /var/www/user 6 | RUN npm install 7 | CMD npm run build && npm run start:prod 8 | -------------------------------------------------------------------------------- /user/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "rimraf dist && tsc -p tsconfig.build.json", 9 | "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env\"", 10 | "start:test": "tsc-watch -p tsconfig.build.json --onSuccess \"node -r dotenv/config dist/main.js dotenv_config_path=../.env.test\"", 11 | "start:prod": "node dist/main.js", 12 | "lint": "eslint \"src/**/*.ts\" --fix" 13 | }, 14 | "dependencies": { 15 | "@nestjs/common": "8.0.0", 16 | "@nestjs/core": "8.0.0", 17 | "@nestjs/microservices": "8.0.0", 18 | "@nestjs/mongoose": "7.2.4", 19 | "@nestjs/platform-express": "8.0.0", 20 | "bcrypt": "5.0.0", 21 | "mongoose": "5.11.15", 22 | "reflect-metadata": "0.1.13", 23 | "rimraf": "3.0.2", 24 | "rxjs": "7.3.0" 25 | }, 26 | "devDependencies": { 27 | "@types/express": "4.17.8", 28 | "@types/node": "14.0.27", 29 | "dotenv": "8.2.0", 30 | "ts-node": "9.0.0", 31 | "tsc-watch": "4.2.9", 32 | "tsconfig-paths": "3.9.0", 33 | "typescript": "4.0.5", 34 | "prettier": "2.1.2", 35 | "eslint-config-prettier": "7.0.0", 36 | "eslint-plugin-prettier": "^3.1.4", 37 | "@typescript-eslint/eslint-plugin": "4.6.1", 38 | "@typescript-eslint/parser": "4.6.1", 39 | "eslint": "7.12.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /user/src/interfaces/user-confirm-response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUserConfirmResponse { 2 | status: number; 3 | message: string; 4 | errors: { [key: string]: any } | null; 5 | } 6 | -------------------------------------------------------------------------------- /user/src/interfaces/user-create-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './user.interface'; 2 | 3 | export interface IUserCreateResponse { 4 | status: number; 5 | message: string; 6 | user: IUser | null; 7 | errors: { [key: string]: any } | null; 8 | } 9 | -------------------------------------------------------------------------------- /user/src/interfaces/user-link.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export interface IUserLink extends Document { 4 | id?: string; 5 | user_id: string; 6 | link: string; 7 | is_used: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /user/src/interfaces/user-search-response.interface.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from './user.interface'; 2 | 3 | export interface IUserSearchResponse { 4 | status: number; 5 | message: string; 6 | user: IUser | null; 7 | } 8 | -------------------------------------------------------------------------------- /user/src/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export interface IUser extends Document { 4 | id?: string; 5 | email: string; 6 | password: string; 7 | is_confirmed: boolean; 8 | compareEncryptedPassword: (password: string) => boolean; 9 | getEncryptedPassword: (password: string) => string; 10 | } 11 | -------------------------------------------------------------------------------- /user/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { UserModule } from './user.module'; 3 | import { Transport, TcpOptions } from '@nestjs/microservices'; 4 | 5 | import { ConfigService } from './services/config/config.service'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.createMicroservice(UserModule, { 9 | transport: Transport.TCP, 10 | options: { 11 | host: '0.0.0.0', 12 | port: new ConfigService().get('port'), 13 | }, 14 | } as TcpOptions); 15 | await app.listenAsync(); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /user/src/schemas/user-link.schema.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | 3 | function transformValue(doc, ret: { [key: string]: any }) { 4 | delete ret._id; 5 | } 6 | 7 | function generateLink() { 8 | return Math.random().toString(36).replace('0.', ''); 9 | } 10 | 11 | export const UserLinkSchema = new mongoose.Schema( 12 | { 13 | user_id: { 14 | type: String, 15 | required: [true, 'User can not be empty'], 16 | }, 17 | is_used: { 18 | type: Boolean, 19 | default: false, 20 | }, 21 | link: { 22 | type: String, 23 | default: generateLink(), 24 | }, 25 | }, 26 | { 27 | toObject: { 28 | virtuals: true, 29 | versionKey: false, 30 | transform: transformValue, 31 | }, 32 | toJSON: { 33 | virtuals: true, 34 | versionKey: false, 35 | transform: transformValue, 36 | }, 37 | }, 38 | ); 39 | -------------------------------------------------------------------------------- /user/src/schemas/user.schema.ts: -------------------------------------------------------------------------------- 1 | import * as mongoose from 'mongoose'; 2 | import * as bcrypt from 'bcrypt'; 3 | 4 | const SALT_ROUNDS = 10; 5 | 6 | function transformValue(doc, ret: { [key: string]: any }) { 7 | delete ret._id; 8 | delete ret.password; 9 | } 10 | 11 | export interface IUserSchema extends mongoose.Document { 12 | email: string; 13 | password: string; 14 | is_confirmed: boolean; 15 | comparePassword: (password: string) => Promise; 16 | getEncryptedPassword: (password: string) => Promise; 17 | } 18 | 19 | export const UserSchema = new mongoose.Schema( 20 | { 21 | email: { 22 | type: String, 23 | required: [true, 'Email can not be empty'], 24 | match: [ 25 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 26 | 'Email should be valid', 27 | ], 28 | }, 29 | is_confirmed: { 30 | type: Boolean, 31 | required: [true, 'Confirmed can not be empty'], 32 | }, 33 | password: { 34 | type: String, 35 | required: [true, 'Password can not be empty'], 36 | minlength: [6, 'Password should include at least 6 chars'], 37 | }, 38 | }, 39 | { 40 | toObject: { 41 | virtuals: true, 42 | versionKey: false, 43 | transform: transformValue, 44 | }, 45 | toJSON: { 46 | virtuals: true, 47 | versionKey: false, 48 | transform: transformValue, 49 | }, 50 | }, 51 | ); 52 | 53 | UserSchema.methods.getEncryptedPassword = ( 54 | password: string, 55 | ): Promise => { 56 | return bcrypt.hash(String(password), SALT_ROUNDS); 57 | }; 58 | 59 | UserSchema.methods.compareEncryptedPassword = function (password: string) { 60 | return bcrypt.compare(password, this.password); 61 | }; 62 | 63 | UserSchema.pre('save', async function (next) { 64 | if (!this.isModified('password')) { 65 | return next(); 66 | } 67 | this.password = await this.getEncryptedPassword(this.password); 68 | next(); 69 | }); 70 | -------------------------------------------------------------------------------- /user/src/services/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Transport } from '@nestjs/microservices'; 2 | 3 | export class ConfigService { 4 | private readonly envConfig: { [key: string]: any } = null; 5 | 6 | constructor() { 7 | this.envConfig = { 8 | port: process.env.USER_SERVICE_PORT, 9 | }; 10 | this.envConfig.baseUri = process.env.BASE_URI; 11 | this.envConfig.gatewayPort = process.env.API_GATEWAY_PORT; 12 | this.envConfig.mailerService = { 13 | options: { 14 | port: process.env.MAILER_SERVICE_PORT, 15 | host: process.env.MAILER_SERVICE_HOST, 16 | }, 17 | transport: Transport.TCP, 18 | }; 19 | } 20 | 21 | get(key: string): any { 22 | return this.envConfig[key]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /user/src/services/config/mongo-config.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MongooseOptionsFactory, 3 | MongooseModuleOptions, 4 | } from '@nestjs/mongoose'; 5 | 6 | export class MongoConfigService implements MongooseOptionsFactory { 7 | createMongooseOptions(): MongooseModuleOptions { 8 | return { 9 | uri: process.env.MONGO_DSN, 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /user/src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | 5 | import { ConfigService } from './config/config.service'; 6 | import { IUser } from '../interfaces/user.interface'; 7 | import { IUserLink } from '../interfaces/user-link.interface'; 8 | 9 | @Injectable() 10 | export class UserService { 11 | constructor( 12 | @InjectModel('User') private readonly userModel: Model, 13 | @InjectModel('UserLink') private readonly userLinkModel: Model, 14 | private readonly configService: ConfigService, 15 | ) {} 16 | 17 | public async searchUser(params: { email: string }): Promise { 18 | return this.userModel.find(params).exec(); 19 | } 20 | 21 | public async searchUserById(id: string): Promise { 22 | return this.userModel.findById(id).exec(); 23 | } 24 | 25 | public async updateUserById( 26 | id: string, 27 | userParams: { is_confirmed: boolean }, 28 | ): Promise { 29 | return this.userModel.updateOne({ _id: id }, userParams).exec(); 30 | } 31 | 32 | public async createUser(user: IUser): Promise { 33 | const userModel = new this.userModel(user); 34 | return await userModel.save(); 35 | } 36 | 37 | public async createUserLink(id: string): Promise { 38 | const userLinkModel = new this.userLinkModel({ 39 | user_id: id, 40 | }); 41 | return await userLinkModel.save(); 42 | } 43 | 44 | public async getUserLink(link: string): Promise { 45 | return this.userLinkModel.find({ link, is_used: false }).exec(); 46 | } 47 | 48 | public async updateUserLinkById( 49 | id: string, 50 | linkParams: { is_used: boolean }, 51 | ): Promise { 52 | return this.userLinkModel.updateOne({ _id: id }, linkParams); 53 | } 54 | 55 | public getConfirmationLink(link: string): string { 56 | return `${this.configService.get('baseUri')}:${this.configService.get( 57 | 'gatewayPort', 58 | )}/users/confirm/${link}`; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /user/src/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, HttpStatus, Inject } from '@nestjs/common'; 2 | import { MessagePattern, ClientProxy } from '@nestjs/microservices'; 3 | 4 | import { UserService } from './services/user.service'; 5 | import { IUser } from './interfaces/user.interface'; 6 | import { IUserCreateResponse } from './interfaces/user-create-response.interface'; 7 | import { IUserSearchResponse } from './interfaces/user-search-response.interface'; 8 | import { IUserConfirmResponse } from './interfaces/user-confirm-response.interface'; 9 | 10 | @Controller('user') 11 | export class UserController { 12 | constructor( 13 | private readonly userService: UserService, 14 | @Inject('MAILER_SERVICE') private readonly mailerServiceClient: ClientProxy, 15 | ) {} 16 | 17 | @MessagePattern('user_search_by_credentials') 18 | public async searchUserByCredentials(searchParams: { 19 | email: string; 20 | password: string; 21 | }): Promise { 22 | let result: IUserSearchResponse; 23 | 24 | if (searchParams.email && searchParams.password) { 25 | const user = await this.userService.searchUser({ 26 | email: searchParams.email, 27 | }); 28 | 29 | if (user && user[0]) { 30 | if (await user[0].compareEncryptedPassword(searchParams.password)) { 31 | result = { 32 | status: HttpStatus.OK, 33 | message: 'user_search_by_credentials_success', 34 | user: user[0], 35 | }; 36 | } else { 37 | result = { 38 | status: HttpStatus.NOT_FOUND, 39 | message: 'user_search_by_credentials_not_match', 40 | user: null, 41 | }; 42 | } 43 | } else { 44 | result = { 45 | status: HttpStatus.NOT_FOUND, 46 | message: 'user_search_by_credentials_not_found', 47 | user: null, 48 | }; 49 | } 50 | } else { 51 | result = { 52 | status: HttpStatus.NOT_FOUND, 53 | message: 'user_search_by_credentials_not_found', 54 | user: null, 55 | }; 56 | } 57 | 58 | return result; 59 | } 60 | 61 | @MessagePattern('user_get_by_id') 62 | public async getUserById(id: string): Promise { 63 | let result: IUserSearchResponse; 64 | 65 | if (id) { 66 | const user = await this.userService.searchUserById(id); 67 | if (user) { 68 | result = { 69 | status: HttpStatus.OK, 70 | message: 'user_get_by_id_success', 71 | user, 72 | }; 73 | } else { 74 | result = { 75 | status: HttpStatus.NOT_FOUND, 76 | message: 'user_get_by_id_not_found', 77 | user: null, 78 | }; 79 | } 80 | } else { 81 | result = { 82 | status: HttpStatus.BAD_REQUEST, 83 | message: 'user_get_by_id_bad_request', 84 | user: null, 85 | }; 86 | } 87 | 88 | return result; 89 | } 90 | 91 | @MessagePattern('user_confirm') 92 | public async confirmUser(confirmParams: { 93 | link: string; 94 | }): Promise { 95 | let result: IUserConfirmResponse; 96 | 97 | if (confirmParams) { 98 | const userLink = await this.userService.getUserLink(confirmParams.link); 99 | 100 | if (userLink && userLink[0]) { 101 | const userId = userLink[0].user_id; 102 | await this.userService.updateUserById(userId, { 103 | is_confirmed: true, 104 | }); 105 | await this.userService.updateUserLinkById(userLink[0].id, { 106 | is_used: true, 107 | }); 108 | result = { 109 | status: HttpStatus.OK, 110 | message: 'user_confirm_success', 111 | errors: null, 112 | }; 113 | } else { 114 | result = { 115 | status: HttpStatus.NOT_FOUND, 116 | message: 'user_confirm_not_found', 117 | errors: null, 118 | }; 119 | } 120 | } else { 121 | result = { 122 | status: HttpStatus.BAD_REQUEST, 123 | message: 'user_confirm_bad_request', 124 | errors: null, 125 | }; 126 | } 127 | 128 | return result; 129 | } 130 | 131 | @MessagePattern('user_create') 132 | public async createUser(userParams: IUser): Promise { 133 | let result: IUserCreateResponse; 134 | 135 | if (userParams) { 136 | const usersWithEmail = await this.userService.searchUser({ 137 | email: userParams.email, 138 | }); 139 | 140 | if (usersWithEmail && usersWithEmail.length > 0) { 141 | result = { 142 | status: HttpStatus.CONFLICT, 143 | message: 'user_create_conflict', 144 | user: null, 145 | errors: { 146 | email: { 147 | message: 'Email already exists', 148 | path: 'email', 149 | }, 150 | }, 151 | }; 152 | } else { 153 | try { 154 | userParams.is_confirmed = false; 155 | const createdUser = await this.userService.createUser(userParams); 156 | const userLink = await this.userService.createUserLink( 157 | createdUser.id, 158 | ); 159 | delete createdUser.password; 160 | result = { 161 | status: HttpStatus.CREATED, 162 | message: 'user_create_success', 163 | user: createdUser, 164 | errors: null, 165 | }; 166 | this.mailerServiceClient 167 | .send('mail_send', { 168 | to: createdUser.email, 169 | subject: 'Email confirmation', 170 | html: `
171 | Hi there, please confirm your email to use Smoothday.
172 | Use the following link for this.
173 | Confirm The Email 176 |
`, 177 | }) 178 | .toPromise(); 179 | } catch (e) { 180 | result = { 181 | status: HttpStatus.PRECONDITION_FAILED, 182 | message: 'user_create_precondition_failed', 183 | user: null, 184 | errors: e.errors, 185 | }; 186 | } 187 | } 188 | } else { 189 | result = { 190 | status: HttpStatus.BAD_REQUEST, 191 | message: 'user_create_bad_request', 192 | user: null, 193 | errors: null, 194 | }; 195 | } 196 | 197 | return result; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /user/src/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ClientProxyFactory } from '@nestjs/microservices'; 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | import { UserController } from './user.controller'; 5 | import { UserService } from './services/user.service'; 6 | import { MongoConfigService } from './services/config/mongo-config.service'; 7 | import { ConfigService } from './services/config/config.service'; 8 | import { UserSchema } from './schemas/user.schema'; 9 | import { UserLinkSchema } from './schemas/user-link.schema'; 10 | 11 | @Module({ 12 | imports: [ 13 | MongooseModule.forRootAsync({ 14 | useClass: MongoConfigService, 15 | }), 16 | MongooseModule.forFeature([ 17 | { 18 | name: 'User', 19 | schema: UserSchema, 20 | collection: 'users', 21 | }, 22 | { 23 | name: 'UserLink', 24 | schema: UserLinkSchema, 25 | collection: 'user_links', 26 | }, 27 | ]), 28 | ], 29 | controllers: [UserController], 30 | providers: [ 31 | UserService, 32 | ConfigService, 33 | { 34 | provide: 'MAILER_SERVICE', 35 | useFactory: (configService: ConfigService) => { 36 | const mailerServiceOptions = configService.get('mailerService'); 37 | return ClientProxyFactory.create(mailerServiceOptions); 38 | }, 39 | inject: [ConfigService], 40 | }, 41 | ], 42 | }) 43 | export class UserModule {} 44 | -------------------------------------------------------------------------------- /user/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | --------------------------------------------------------------------------------