init from boilerplate
This commit is contained in:
parent
55570dee59
commit
72d7659219
31
.dockerignore
Normal file
31
.dockerignore
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#husky
|
||||||
|
|
||||||
|
# Build dependencies
|
||||||
|
node_modules/
|
||||||
|
coverage/
|
||||||
|
coverage-e2e/
|
||||||
|
coverage-integration/
|
||||||
|
dist/
|
||||||
|
.husky/
|
||||||
|
.github/
|
||||||
|
prod/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Environment (contains sensitive data)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Versioning and metadata
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
.eslintignore
|
||||||
|
|
||||||
|
# Files not required for production
|
||||||
|
.editorconfig
|
||||||
|
dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
cspell.json
|
||||||
|
README.md
|
||||||
|
nodemon.json
|
||||||
52
.env.example
Normal file
52
.env.example
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
APP_NAME=NEST
|
||||||
|
APP_ENV=development
|
||||||
|
APP_LANGUAGE=en
|
||||||
|
|
||||||
|
HTTP_ENABLE=true
|
||||||
|
HTTP_HOST=localhost
|
||||||
|
HTTP_PORT= 3000
|
||||||
|
HTTP_VERSIONING_ENABLE=true
|
||||||
|
HTTP_VERSION=1
|
||||||
|
|
||||||
|
DEBUGGER_HTTP_WRITE_INTO_FILE=false
|
||||||
|
DEBUGGER_HTTP_WRITE_INTO_CONSOLE=false
|
||||||
|
DEBUGGER_SYSTEM_WRITE_INTO_FILE=false
|
||||||
|
DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE=false
|
||||||
|
|
||||||
|
JOB_ENABLE=false
|
||||||
|
|
||||||
|
DATABASE_HOST=mongodb://localhost:30001,localhost:30002,localhost:30003
|
||||||
|
DATABASE_NAME=nest
|
||||||
|
DATABASE_USER=
|
||||||
|
DATABASE_PASSWORD=
|
||||||
|
DATABASE_DEBUG=false
|
||||||
|
DATABASE_OPTIONS=replicaSet=rs0&retryWrites=true&w=majority
|
||||||
|
|
||||||
|
AUTH_JWT_SUBJECT=nestDevelopment
|
||||||
|
AUTH_JWT_ISSUER=nest
|
||||||
|
AUTH_JWT_AUDIENCE=https://example.com
|
||||||
|
|
||||||
|
AUTH_JWT_ACCESS_TOKEN_SECRET_KEY=85huyujDurLdvLsjAW93XsqP79rAotqplHCOEWj1wzyIcMtT
|
||||||
|
AUTH_JWT_ACCESS_TOKEN_EXPIRED=15m
|
||||||
|
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_SECRET_KEY=7Y3nqaO8jKVOFBRy9ujn5uUxV8Iy2otHrnQgiXlIGAqiVdb5
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_EXPIRED=7d
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED=30d
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION=15m
|
||||||
|
|
||||||
|
AUTH_PERMISSION_TOKEN_SECRET_KEY=85huyujDurLdvLsjAW93XsqP79rAotqplHCOEWj1wzyIcMtT
|
||||||
|
AUTH_PERMISSION_TOKEN_EXPIRED=5m
|
||||||
|
|
||||||
|
AUTH_JWT_PAYLOAD_ENCRYPT=false
|
||||||
|
AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY=fKyRq7g9eftVNEdiCC7lNU6fga5Pr1iC7dc0JYsC
|
||||||
|
AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV=mLZZdQrXqjPW5F5H2eko
|
||||||
|
AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY=NnCnSrRmw5YuQyTPtDokOWmKR37EYbuB6ITZqqZd
|
||||||
|
AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV=eP7P8Pmvq207zhyd61dz
|
||||||
|
|
||||||
|
AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY=hUcRIUQzJMe17w8cAZAreMdjxjo1JbucBACu7tAw
|
||||||
|
AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV=7V0D5a0D3SdsgM1KT5rF
|
||||||
|
|
||||||
|
AWS_CREDENTIAL_KEY=
|
||||||
|
AWS_CREDENTIAL_SECRET=
|
||||||
|
AWS_S3_REGION=ap-southeast-3
|
||||||
|
AWS_S3_BUCKET=baibay-development
|
||||||
9
.eslintignore
Normal file
9
.eslintignore
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# /node_modules/* in the project root is ignored by default
|
||||||
|
# build artefacts
|
||||||
|
dist/*
|
||||||
|
coverage/*
|
||||||
|
node_modules/*
|
||||||
|
logs/*
|
||||||
|
prod/*
|
||||||
|
.husky/*
|
||||||
|
.github/*
|
||||||
23
.eslintrc
Normal file
23
.eslintrc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "tsconfig.json",
|
||||||
|
"tsconfigRootDir": ".",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint/eslint-plugin"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"ignorePatterns": [".eslintrc.js"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: tuesday
|
||||||
|
time: "00:00"
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
target-branch: "development"
|
||||||
|
commit-message:
|
||||||
|
prefix: "github-action"
|
||||||
|
labels:
|
||||||
|
- dependabot
|
||||||
31
.github/workflows/linter.yml
vendored
Normal file
31
.github/workflows/linter.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
name: Linter
|
||||||
|
on:
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- development
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linter:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: ['18.x']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Git checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup node version ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Linter
|
||||||
|
run: yarn lint
|
||||||
45
.github/workflows/release.yml
vendored
Normal file
45
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
|
||||||
|
# push:
|
||||||
|
# branches:
|
||||||
|
# - main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Git checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get short sha commit
|
||||||
|
id: git
|
||||||
|
run: |
|
||||||
|
echo "short_sha=$(git rev-parse --short $GITHUB_SHA)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Get latest version
|
||||||
|
id: version
|
||||||
|
uses: martinbeentjes/npm-get-version-action@main
|
||||||
|
|
||||||
|
- name: Git
|
||||||
|
run: |
|
||||||
|
echo Branch name is: ${{ github.ref_name }}
|
||||||
|
echo Short sha: ${{ steps.git.outputs.short_sha }}
|
||||||
|
echo Version is: ${{ steps.version.outputs.current-version }}
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@master
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: v${{ steps.version.outputs.current-version }}
|
||||||
|
name: v${{ steps.version.outputs.current-version }}
|
||||||
|
generate_release_notes: true
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
51
.gitignore
vendored
51
.gitignore
vendored
@ -1,4 +1,53 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
/node_modules
|
/node_modules
|
||||||
/coverage
|
.warmup/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/coverage-e2e
|
||||||
|
/coverage-integration
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
.yarn/*
|
||||||
|
.pnp.*
|
||||||
|
!.yarn/cache
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# environment
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
config.yaml
|
||||||
|
config.yml
|
||||||
6
.husky/pre-commit
Normal file
6
.husky/pre-commit
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn lint
|
||||||
|
yarn deadcode
|
||||||
|
yarn spell
|
||||||
@ -1,4 +0,0 @@
|
|||||||
./docs
|
|
||||||
./scripts
|
|
||||||
./tests
|
|
||||||
./incoming
|
|
||||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
9
LICENSE
9
LICENSE
@ -1,9 +0,0 @@
|
|||||||
Copyright (c) <year> <owner> All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) [2023] [Fedi DAYEG]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
515
README.md
515
README.md
@ -1,3 +1,514 @@
|
|||||||
# osr-package-template
|
[![Contributors][nest-contributors-shield]][nest-contributors]
|
||||||
|
[![Forks][nest-forks-shield]][nest-forks]
|
||||||
|
[![Stargazers][nest-stars-shield]][nest-stars]
|
||||||
|
[![Issues][nest-issues-shield]][nest-issues]
|
||||||
|
[![MIT License][nest-license-shield]][license]
|
||||||
|
|
||||||
Package basics
|
[![NestJs][nestjs-shield]][ref-nestjs]
|
||||||
|
[![NodeJs][nodejs-shield]][ref-nodejs]
|
||||||
|
[![Typescript][typescript-shield]][ref-typescript]
|
||||||
|
[![MongoDB][mongodb-shield]][ref-mongodb]
|
||||||
|
[![JWT][jwt-shield]][ref-jwt]
|
||||||
|
[![Jest][jest-shield]][ref-jest]
|
||||||
|
[![Yarn][yarn-shield]][ref-yarn]
|
||||||
|
[![Docker][docker-shield]][ref-docker]
|
||||||
|
|
||||||
|
# Complete NestJs Boilerplate 🔥 🚀
|
||||||
|
|
||||||
|
> This repo will representative of authentication service and authorization service
|
||||||
|
|
||||||
|
[Complete NestJs][nest] is a [Http NestJs v9.x][ref-nestjs] boilerplate. Best uses for backend service.
|
||||||
|
|
||||||
|
*You can [request feature][nest-issues] or [report bug][nest-issues] with following this link*
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
* [Important](#important)
|
||||||
|
* [Next Todo](#next-todo)
|
||||||
|
* [Build With](#build-with)
|
||||||
|
* [Objective](#objective)
|
||||||
|
* [Features](#features)
|
||||||
|
* [Structure](#structure)
|
||||||
|
* [Folder Structure](#folder-structure)
|
||||||
|
* [Module Structure](#module-structure)
|
||||||
|
* [Response Structure](#response-structure)
|
||||||
|
* [Prerequisites](#prerequisites)
|
||||||
|
* [Getting Started](#getting-started)
|
||||||
|
* [Clone Repo](#clone-repo)
|
||||||
|
* [Install Dependencies](#install-dependencies)
|
||||||
|
* [Create environment](#create-environment)
|
||||||
|
* [Database Migration](#database-migration)
|
||||||
|
* [Test](#test)
|
||||||
|
* [Run Project](#run-project)
|
||||||
|
* [Run Project with Docker](#run-project-with-docker)
|
||||||
|
* [API Reference](#api-reference)
|
||||||
|
* [Environment](#environment)
|
||||||
|
* [Api Key Encryption](#api-key-encryption)
|
||||||
|
* [Adjust Mongoose Setting](#adjust-mongoose-setting)
|
||||||
|
* [License](#license)
|
||||||
|
* [Contact](#contact)
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
* The features will replated with AWS Features
|
||||||
|
* If you want to implement `database transactions`, you must run MongoDB as a `replication set`.
|
||||||
|
* If you change the environment value of `APP_ENV` to `production`, that will trigger.
|
||||||
|
1. CorsMiddleware will implement `src/configs/middleware.config.ts`.
|
||||||
|
2. Documentation will `disable`.
|
||||||
|
|
||||||
|
## Next Todo
|
||||||
|
|
||||||
|
Next development
|
||||||
|
|
||||||
|
* [x] serialization update
|
||||||
|
* [x] update unit test for common modules
|
||||||
|
* [x] user e2e
|
||||||
|
* [x] api key for saas, change `x-api-key` to `${key}:${secret}`
|
||||||
|
* [ ] user plan module
|
||||||
|
* [ ] invoicing plan module
|
||||||
|
* [ ] excel decorator optimize
|
||||||
|
* [ ] Authorization optimize, remove permission entity and implement policy guard
|
||||||
|
* [ ] Google SSO
|
||||||
|
* [ ] Background export/import from/to CSV and Excel
|
||||||
|
* [ ] Update Documentation, include an diagram for easier comprehension
|
||||||
|
|
||||||
|
## Build with
|
||||||
|
|
||||||
|
Describes which version.
|
||||||
|
|
||||||
|
| Name | Version |
|
||||||
|
| ---------- | -------- |
|
||||||
|
| NestJs | v9.3.x |
|
||||||
|
| NodeJs | v18.12.x |
|
||||||
|
| Typescript | v5.0.x |
|
||||||
|
| Mongoose | v7.0.x |
|
||||||
|
| MongoDB | v6.0.x |
|
||||||
|
| Yarn | v1.22.x |
|
||||||
|
| NPM | v8.19.x |
|
||||||
|
| Docker | v20.10.x |
|
||||||
|
| Docker Compose | v2.6.x |
|
||||||
|
| Swagger | v6.2.x |
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
* Easy to maintenance
|
||||||
|
* NestJs Habit
|
||||||
|
* Component based folder structure
|
||||||
|
* Stateless authentication and authorization
|
||||||
|
* Repository Design Pattern or Data Access Layer Design Pattern
|
||||||
|
* Follow Community Guide Line
|
||||||
|
* Follow The Twelve-Factor App
|
||||||
|
* Adopt SOLID and KISS principle
|
||||||
|
* Support Microservice Architecture, Serverless Architecture, Clean Architecture, and/or Hexagonal Architecture
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Main Features
|
||||||
|
|
||||||
|
* NestJs v9.x 🥳
|
||||||
|
* Typescript 🚀
|
||||||
|
* Production ready 🔥
|
||||||
|
* Repository Design Pattern (Multi Repository, can mix with `TypeORM`)
|
||||||
|
* Swagger / OpenAPI 3 included
|
||||||
|
* Authentication (`Access Token`, `Refresh Token`, `API Key`, and `Google SSO`)
|
||||||
|
* Authorization, Role and Permission Management (`PermissionToken`)
|
||||||
|
* Support multi-language `i18n` 🗣, can controllable with request header `x-custom-lang`
|
||||||
|
* Request validation for all request params, query, dan body with `class-validation`
|
||||||
|
* Serialization with `class-transformer`
|
||||||
|
* Url Versioning, default version is `1`
|
||||||
|
* Server Side Pagination
|
||||||
|
* Import and export data with CSV or Excel by using `decorator`
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
* MongoDB integrate by using [mongoose][ref-mongoose] 🎉
|
||||||
|
* Multi Database
|
||||||
|
* Database Transaction
|
||||||
|
* Database Soft Delete
|
||||||
|
* Database Migration
|
||||||
|
|
||||||
|
### Logger and Debugger
|
||||||
|
|
||||||
|
* Logger with `Morgan`
|
||||||
|
* Debugger with `Winston` 📝
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
* Apply `helmet`, `cors`, and `rate-limit`
|
||||||
|
* Timeout awareness and can override ⌛️
|
||||||
|
* User agent awareness, and can whitelist user agent
|
||||||
|
|
||||||
|
### Setting
|
||||||
|
|
||||||
|
* Support environment file
|
||||||
|
* Centralize configuration 🤖
|
||||||
|
* Centralize response
|
||||||
|
* Centralize exception filter
|
||||||
|
* Setting from database 🗿
|
||||||
|
|
||||||
|
### Third Party Integration
|
||||||
|
|
||||||
|
* Storage integration with `AwsS3`
|
||||||
|
* Upload file `single` and `multipart` to AwsS3
|
||||||
|
|
||||||
|
### Others
|
||||||
|
|
||||||
|
* Support Docker installation
|
||||||
|
* Support CI/CD with Github Action or Jenkins
|
||||||
|
* Husky GitHook for check source code, and run test before commit 🐶
|
||||||
|
* Linter with EsLint for Typescript
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
### Folder Structure
|
||||||
|
|
||||||
|
1. `/app` The final wrapper module
|
||||||
|
2. `/common` The common module
|
||||||
|
3. `/configs` The configurations for this project
|
||||||
|
4. `/health` health check module for every service integrated
|
||||||
|
5. `/jobs` cron job or schedule task
|
||||||
|
6. `/language` json languages
|
||||||
|
7. `/migration` migrate all init data
|
||||||
|
8. `/modules` other modules based on service based on project
|
||||||
|
9. `/router` endpoint router. `Controller` will put in this
|
||||||
|
|
||||||
|
### Module structure
|
||||||
|
|
||||||
|
Full structure of module
|
||||||
|
|
||||||
|
```txt
|
||||||
|
.
|
||||||
|
└── module1
|
||||||
|
├── abstracts
|
||||||
|
├── constants // constant like enum, static value, status code, etc
|
||||||
|
├── controllers // business logic for rest api
|
||||||
|
├── decorators // warper decorator, custom decorator, etc
|
||||||
|
├── dtos // request validation
|
||||||
|
├── docs // swagger / OpenAPI 3
|
||||||
|
├── errors // custom error
|
||||||
|
├── filters // custom filter
|
||||||
|
├── guards // validate related with database
|
||||||
|
├── indicators // custom health check indicator
|
||||||
|
├── interceptors // custom interceptors
|
||||||
|
├── interfaces
|
||||||
|
├── middleware
|
||||||
|
├── pipes
|
||||||
|
├── repository
|
||||||
|
├── entities // database entities
|
||||||
|
├── repositories // database repositories
|
||||||
|
└── module1.repository.module.ts
|
||||||
|
├── serializations // response serialization
|
||||||
|
├── services
|
||||||
|
├── tasks // task for cron job
|
||||||
|
└── module1.module.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Structure
|
||||||
|
|
||||||
|
This section will describe the structure of the response.
|
||||||
|
|
||||||
|
#### Response Default
|
||||||
|
|
||||||
|
Default response for the response
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export class ResponseDefaultSerialization {
|
||||||
|
statusCode: number;
|
||||||
|
message: string;
|
||||||
|
_metadata?: IResponseMetadata;
|
||||||
|
data?: Record<string, any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Paging
|
||||||
|
|
||||||
|
Default response for pagination.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export class ResponsePagingSerialization {
|
||||||
|
statusCode: number;
|
||||||
|
message: string;
|
||||||
|
totalData: number;
|
||||||
|
totalPage?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
perPage?: number;
|
||||||
|
_availableSearch?: string[];
|
||||||
|
_availableSort?: string[];
|
||||||
|
_metadata?: IResponseMetadata;
|
||||||
|
data: Record<string, any>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response Metadata
|
||||||
|
|
||||||
|
This is useful when we need to give the frontend some information that is related / not related with the endpoint.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface IResponseMetadata {
|
||||||
|
languages: ENUM_MESSAGE_LANGUAGE[];
|
||||||
|
timestamp: number;
|
||||||
|
timezone: string;
|
||||||
|
requestId: string;
|
||||||
|
path: string;
|
||||||
|
version: string;
|
||||||
|
repoVersion: string;
|
||||||
|
nextPage?: string;
|
||||||
|
previousPage?: string;
|
||||||
|
firstPage?: string;
|
||||||
|
lastPage?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
We assume that everyone who comes here is **`programmer with intermediate knowledge`** and we also need to understand more before we begin in order to reduce the knowledge gap.
|
||||||
|
|
||||||
|
1. Understand [NestJs Fundamental][ref-nestjs], Main Framework. NodeJs Framework with support fully TypeScript.
|
||||||
|
2. Understand[Typescript Fundamental][ref-typescript], Programming Language. It will help us to write and read the code.
|
||||||
|
3. Understand [ExpressJs Fundamental][ref-nodejs], NodeJs Base Framework. It will help us in understanding how the NestJs Framework works.
|
||||||
|
4. Understand what NoSql is and how it works as a database, especially [MongoDB.][ref-mongodb]
|
||||||
|
5. Understand Repository Design Pattern or Data Access Object Design Pattern. It will help to read, and write the source code
|
||||||
|
6. Understand The SOLID Principle and KISS Principle for better write the code.
|
||||||
|
7. Optional. Understand Microservice Architecture, Clean Architecture, and/or Hexagonal Architecture. It can help to serve the project.
|
||||||
|
8. Optional. Understanding [The Twelve Factor Apps][ref-12factor]. It can help to serve the project.
|
||||||
|
9. Optional. Understanding [Docker][ref-docker]. It can help to run the project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Before start, we need to install some packages and tools.
|
||||||
|
The recommended version is the LTS version for every tool and package.
|
||||||
|
|
||||||
|
> Make sure to check that the tools have been installed successfully.
|
||||||
|
|
||||||
|
1. [NodeJs][ref-nodejs]
|
||||||
|
2. [MongoDB][ref-mongodb]
|
||||||
|
3. [Yarn][ref-yarn]
|
||||||
|
4. [Git][ref-git]
|
||||||
|
|
||||||
|
### Clone Repo
|
||||||
|
|
||||||
|
Clone the project with git.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/fedi-dayeg/complete-nestjs-boilerplate.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Dependencies
|
||||||
|
|
||||||
|
This project needs some dependencies. Let's go install it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create environment
|
||||||
|
|
||||||
|
Make your own environment file with a copy of `env.example` and adjust values to suit your own environment.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
To know the details, you can read the documentation. [Jump to document section](#documentation)
|
||||||
|
|
||||||
|
### Database Migration
|
||||||
|
|
||||||
|
> The migration will do data seeding to MongoDB. Make sure to check the value of the `DATABASE_` prefix in your`.env` file.
|
||||||
|
|
||||||
|
The Database migration used [NestJs-Command][ref-nestjscommand]
|
||||||
|
|
||||||
|
For seeding
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn seed
|
||||||
|
```
|
||||||
|
|
||||||
|
For remove all data do
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
> The test is still not good net. I'm still lazy too do that.
|
||||||
|
|
||||||
|
The project provide 3 automation testing `unit testing`, `integration testing`, and `e2e testing`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
For specific test do this
|
||||||
|
|
||||||
|
* Unit testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
* Integration testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test:integration
|
||||||
|
```
|
||||||
|
|
||||||
|
* E2E testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Project
|
||||||
|
|
||||||
|
Finally, Cheers 🍻🍻 !!! you passed all steps.
|
||||||
|
|
||||||
|
Now you can run the project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Project with Docker
|
||||||
|
|
||||||
|
For docker installation, we need more tools to be installed in our instance.
|
||||||
|
|
||||||
|
1. [Docker][ref-docker]
|
||||||
|
2. [Docker-Compose][ref-dockercompose]
|
||||||
|
|
||||||
|
Then run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
You can check The ApiSpec after running this project. [here][api-reference-docs]
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
> Ongoing update
|
||||||
|
|
||||||
|
## Adjust Mongoose Setting
|
||||||
|
|
||||||
|
> Optional, if your mongodb version is < 5
|
||||||
|
|
||||||
|
Go to file `src/common/database/services/database.options.service.ts` and add `useMongoClient` to `mongooseOptions` then set value to `true`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const mongooseOptions: MongooseModuleOptions = {
|
||||||
|
uri,
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
serverSelectionTimeoutMS: 5000,
|
||||||
|
useMongoClient: true // <--- add this
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Distributed under [MIT licensed][license].
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
How to contribute in this repo
|
||||||
|
|
||||||
|
1. Fork the project with click `Fork` button of this repo.
|
||||||
|
2. Clone the fork project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone "url you just copied"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Make necessary changes and commit those changes
|
||||||
|
4. Commit the changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "your message"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Push changes to fork project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin -u main
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Back to browser, goto your fork repo github. Then, click `Compare & pull request`
|
||||||
|
|
||||||
|
If your code behind commit with the original, please update your code and resolve the conflict. Then, repeat from number 6.
|
||||||
|
|
||||||
|
### Rule
|
||||||
|
|
||||||
|
* Avoid Circular Dependency
|
||||||
|
* Consume component folder structure, and repository design pattern
|
||||||
|
* Always make `service` for every module is independently.
|
||||||
|
* Do not put `controller` into service modules, cause this will break the dependency. Only put the controller into `router` and then inject the dependency.
|
||||||
|
* Put the config in `/configs` folder, and for dynamic config put as `environment variable`
|
||||||
|
* `CommonModule` only for main package, and put the module that related of service/project into `/src/modules`. So, if we want to clear the unnecessary module, we just need to delete the `src/modules/**`
|
||||||
|
* If there a new service in CommonModule. Make sure to create the unit test in `/test/unit`.
|
||||||
|
* If there a new controller, make sure to create the e2e testing in `test/e2e`
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
[Fedi DAYEG][author-email]
|
||||||
|
|
||||||
|
[![Github][github-shield]][author-github]
|
||||||
|
[![LinkedIn][linkedin-shield]][author-linkedin]
|
||||||
|
|
||||||
|
<!-- BADGE LINKS -->
|
||||||
|
[nest-contributors-shield]: https://img.shields.io/github/contributors/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge
|
||||||
|
[nest-forks-shield]: https://img.shields.io/github/forks/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge
|
||||||
|
[nest-stars-shield]: https://img.shields.io/github/stars/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge
|
||||||
|
[nest-issues-shield]: https://img.shields.io/github/issues/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge
|
||||||
|
[nest-license-shield]: https://img.shields.io/github/license/fedi-dayeg/complete-nestjs-boilerplate?style=for-the-badge
|
||||||
|
|
||||||
|
[nestjs-shield]: https://img.shields.io/badge/nestjs-%23E0234E.svg?style=for-the-badge&logo=nestjs&logoColor=white
|
||||||
|
[nodejs-shield]: https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white
|
||||||
|
[typescript-shield]: https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white
|
||||||
|
[mongodb-shield]: https://img.shields.io/badge/MongoDB-white?style=for-the-badge&logo=mongodb&logoColor=4EA94B
|
||||||
|
[jwt-shield]: https://img.shields.io/badge/JWT-000000?style=for-the-badge&logo=JSON%20web%20tokens&logoColor=white
|
||||||
|
[jest-shield]: https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white
|
||||||
|
[yarn-shield]: https://img.shields.io/badge/yarn-%232C8EBB.svg?style=for-the-badge&logo=yarn&logoColor=white
|
||||||
|
[docker-shield]: https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white
|
||||||
|
|
||||||
|
[github-shield]: https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white
|
||||||
|
[linkedin-shield]: https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white
|
||||||
|
|
||||||
|
<!-- CONTACTS -->
|
||||||
|
[author-linkedin]: https://www.linkedin.com/in/fedi-dayeg-a330b6131/
|
||||||
|
[author-email]: mailto:fedi.dayeg@gmail.com
|
||||||
|
[author-github]: https://github.com/fedi-dayeg
|
||||||
|
|
||||||
|
<!-- Repo LINKS -->
|
||||||
|
[nest-issues]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/issues
|
||||||
|
[nest-stars]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/stargazers
|
||||||
|
[nest-forks]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/network/members
|
||||||
|
[nest-contributors]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate/graphs/contributors
|
||||||
|
|
||||||
|
<!-- Other Repo Links -->
|
||||||
|
[nest]: https://github.com/fedi-dayeg/complete-nestjs-boilerplate
|
||||||
|
|
||||||
|
|
||||||
|
<!-- license -->
|
||||||
|
[license]: LICENSE.md
|
||||||
|
|
||||||
|
<!-- Reference -->
|
||||||
|
[ref-nestjs]: http://nestjs.com
|
||||||
|
[ref-mongoose]: https://mongoosejs.com
|
||||||
|
[ref-mongodb]: https://docs.mongodb.com/
|
||||||
|
[ref-nodejs]: https://nodejs.org/
|
||||||
|
[ref-typescript]: https://www.typescriptlang.org/
|
||||||
|
[ref-docker]: https://docs.docker.com
|
||||||
|
[ref-dockercompose]: https://docs.docker.com/compose/
|
||||||
|
[ref-yarn]: https://yarnpkg.com
|
||||||
|
[ref-12factor]: https://12factor.net
|
||||||
|
[ref-nestjscommand]: https://gitlab.com/aa900031/nestjs-command
|
||||||
|
[ref-jwt]: https://jwt.io
|
||||||
|
[ref-jest]: https://jestjs.io/docs/getting-started
|
||||||
|
[ref-git]: https://git-scm.com
|
||||||
|
|
||||||
|
<!-- API Reference -->
|
||||||
|
[api-reference-docs]: http://localhost:3000/docs
|
||||||
|
|||||||
60
cspell.json
Normal file
60
cspell.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2",
|
||||||
|
"language": "en",
|
||||||
|
"words": [
|
||||||
|
"nestjs",
|
||||||
|
"metatype",
|
||||||
|
"virtuals",
|
||||||
|
"prebuild",
|
||||||
|
"logform",
|
||||||
|
"transfomer",
|
||||||
|
"insync",
|
||||||
|
"microservices",
|
||||||
|
"globby",
|
||||||
|
"dockerhub",
|
||||||
|
"fifsky",
|
||||||
|
"buildx",
|
||||||
|
"exceljs",
|
||||||
|
"milis",
|
||||||
|
"workdir",
|
||||||
|
"dbdata",
|
||||||
|
"initdb",
|
||||||
|
"deadcode",
|
||||||
|
"authapis",
|
||||||
|
"headerapikey",
|
||||||
|
"jenkinsfile",
|
||||||
|
"superadmin",
|
||||||
|
"alphanum",
|
||||||
|
"dtos",
|
||||||
|
"typeorm",
|
||||||
|
"apikeys",
|
||||||
|
"apikey",
|
||||||
|
"maxlength",
|
||||||
|
"Streamable",
|
||||||
|
"unallow",
|
||||||
|
"datauser"
|
||||||
|
],
|
||||||
|
"ignoreWords": [
|
||||||
|
"psheon",
|
||||||
|
"aallithioo",
|
||||||
|
"tiaamoo",
|
||||||
|
"qwertyuiop12345zxcvbnmkjh",
|
||||||
|
"opbUwdiS1FBsrDUoPgZdx",
|
||||||
|
"cuwakimacojulawu",
|
||||||
|
"baibay",
|
||||||
|
"acks",
|
||||||
|
"andrechristikan",
|
||||||
|
"trueaaa",
|
||||||
|
"aasdasd"
|
||||||
|
],
|
||||||
|
"ignorePaths": [
|
||||||
|
"node_modules/**",
|
||||||
|
"endpoints/**",
|
||||||
|
"*coverage/**",
|
||||||
|
".husky/**",
|
||||||
|
".github/**",
|
||||||
|
"dist/**",
|
||||||
|
"logs/**",
|
||||||
|
"src/database/json/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
nestService:
|
||||||
|
build: .
|
||||||
|
container_name: nestService
|
||||||
|
hostname: nestService
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
volumes:
|
||||||
|
- ./src/:/app/src/
|
||||||
|
- .env/:/app/.env
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- nestDatabase
|
||||||
|
nestDatabase:
|
||||||
|
image: mongo:latest
|
||||||
|
container_name: nestDatabase
|
||||||
|
hostname: nestDatabase
|
||||||
|
ports:
|
||||||
|
- 27017:27017
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: root
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: 123456
|
||||||
|
MONGO_INITDB_DATABASE: nest
|
||||||
|
volumes:
|
||||||
|
- dbdata:/data/db
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
name: app-network
|
||||||
|
driver: bridge
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
14
dockerfile
Normal file
14
dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM node:lts-alpine
|
||||||
|
LABEL maintainer "fedi.dayeg@gmail.com"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN touch .env
|
||||||
|
|
||||||
|
RUN set -x && yarn
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD [ "yarn", "start:dev" ]
|
||||||
16
nest-cli.json
Normal file
16
nest-cli.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"plugins": ["@nestjs/swagger"],
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"include": "languages/**/*",
|
||||||
|
"outDir": "dist/src"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"webpack": false,
|
||||||
|
"deleteOutDir": true,
|
||||||
|
"watchAssets": false
|
||||||
|
}
|
||||||
|
}
|
||||||
168
package.json
168
package.json
@ -1,45 +1,135 @@
|
|||||||
{
|
{
|
||||||
"name": "@plastichub/template",
|
"name": "nestjs-boilerplate",
|
||||||
"description": "",
|
"version": "1.0.0",
|
||||||
"version": "0.3.1",
|
"description": "NestJs Boilerplate",
|
||||||
"main": "main.js",
|
"repository": {
|
||||||
"typings": "index.d.ts",
|
"type": "git"
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"author": {
|
||||||
"osr-bin": "main.js"
|
"name": "Fedi DAYEG",
|
||||||
|
"email": "fedi.dayeg@gmail.com"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"upgrade:package": "ncu -u",
|
||||||
|
"prebuild": "rimraf dist",
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "yarn format:src && yarn format:test",
|
||||||
|
"format:src": "prettier --write src/**/*.ts",
|
||||||
|
"format:test": "prettier --write test/**/*.ts",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/src/main",
|
||||||
|
"lint": "yarn lint:src && yarn lint:test",
|
||||||
|
"lint:fix": "eslint --ext .ts,.tsx '{src,test}/**/*.ts' --fix --no-error-on-unmatched-pattern",
|
||||||
|
"lint:src": "eslint --ext .ts,.tsx 'src/**/*.ts' --no-error-on-unmatched-pattern",
|
||||||
|
"lint:test": "eslint --ext .ts,.tsx 'test/**/*.ts' --no-error-on-unmatched-pattern",
|
||||||
|
"test": "yarn test:unit && yarn test:integration && yarn test:e2e",
|
||||||
|
"test:unit": "jest --config test/unit/jest.json --passWithNoTests --forceExit",
|
||||||
|
"test:integration": "jest --config test/integration/jest.json --passWithNoTests --forceExit",
|
||||||
|
"test:e2e": "jest --runInBand --config test/e2e/jest.json --verbose --passWithNoTests --forceExit",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"deadcode": "ts-prune --project tsconfig.json --skip *.json",
|
||||||
|
"deadcode:filter": "ts-prune --project tsconfig.json --skip *.json | grep -v '(used in module)'",
|
||||||
|
"deadcode:count": "ts-prune --project tsconfig.json --skip *.json | grep -v '(used in module)' | wc -l",
|
||||||
|
"spell": "yarn spell:src && yarn spell:test",
|
||||||
|
"spell:src": "cspell lint --config cspell.json src/**/*.ts --color --gitignore --no-must-find-files --no-summary --no-progress || true",
|
||||||
|
"spell:test": "cspell lint --config cspell.json test/**/*.ts --color --gitignore --no-must-find-files --no-summary --no-progress || true",
|
||||||
|
"seed:setting": "nestjs-command seed:setting",
|
||||||
|
"seed:apikey": "nestjs-command seed:apikey",
|
||||||
|
"seed:permission": "nestjs-command seed:permission",
|
||||||
|
"seed:role": "nestjs-command seed:role",
|
||||||
|
"seed:user": "nestjs-command seed:user",
|
||||||
|
"rollback:setting": "nestjs-command remove:setting",
|
||||||
|
"rollback:apikey": "nestjs-command remove:apikey",
|
||||||
|
"rollback:permission": "nestjs-command remove:permission",
|
||||||
|
"rollback:role": "nestjs-command remove:role",
|
||||||
|
"rollback:user": "nestjs-command remove:user",
|
||||||
|
"seed": "yarn seed:setting && yarn seed:permission && yarn seed:role && yarn seed:user && yarn seed:apikey",
|
||||||
|
"rollback": "yarn rollback:setting && yarn rollback:apikey && yarn rollback:user && yarn rollback:role && yarn rollback:permission"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^14.17.5",
|
"@aws-sdk/client-s3": "^3.292.0",
|
||||||
"@types/yargs": "^17.0.2",
|
"@faker-js/faker": "^7.6.0",
|
||||||
"chalk": "^2.4.1",
|
"@joi/date": "^2.1.0",
|
||||||
"convert-units": "^2.3.4",
|
"@nestjs/axios": "^2.0.0",
|
||||||
"env-var": "^7.0.1",
|
"@nestjs/common": "^9.3.10",
|
||||||
"typescript": "^4.3.5",
|
"@nestjs/config": "^2.3.1",
|
||||||
"yargs": "^14.2.3",
|
"@nestjs/core": "^9.3.10",
|
||||||
"yargs-parser": "^15.0.3"
|
"@nestjs/jwt": "^10.0.2",
|
||||||
|
"@nestjs/mongoose": "^9.2.1",
|
||||||
|
"@nestjs/passport": "^9.0.3",
|
||||||
|
"@nestjs/platform-express": "^9.3.10",
|
||||||
|
"@nestjs/schedule": "^2.2.0",
|
||||||
|
"@nestjs/swagger": "^6.2.1",
|
||||||
|
"@nestjs/terminus": "^9.2.1",
|
||||||
|
"@nestjs/throttler": "^4.0.0",
|
||||||
|
"@types/response-time": "^2.3.5",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"geolib": "^3.3.3",
|
||||||
|
"helmet": "^6.0.1",
|
||||||
|
"joi": "^17.8.4",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"mongoose": "^7.0.2",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"nest-winston": "^1.9.1",
|
||||||
|
"nestjs-command": "^3.1.3",
|
||||||
|
"nestjs-i18n": "^10.2.6",
|
||||||
|
"passport": "^0.6.0",
|
||||||
|
"passport-headerapikey": "^1.2.2",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"response-time": "^2.3.2",
|
||||||
|
"rimraf": "^4.4.0",
|
||||||
|
"rotating-file-stream": "^3.1.0",
|
||||||
|
"rxjs": "^7.8.0",
|
||||||
|
"ua-parser-js": "^1.0.34",
|
||||||
|
"winston": "^3.8.2",
|
||||||
|
"winston-daily-rotate-file": "^4.7.1",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
|
"yargs": "^17.7.1",
|
||||||
|
"yarn": "^1.22.19"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"devDependencies": {
|
||||||
"test": "tsc; mocha --full-trace mocha \"spec/**/*.spec.js\"",
|
"@nestjs/cli": "^9.2.0",
|
||||||
"test-with-coverage": "istanbul cover node_modules/.bin/_mocha -- 'spec/**/*.spec.js'",
|
"@nestjs/schematics": "^9.0.4",
|
||||||
"lint": "tslint --project=./tsconfig.json",
|
"@nestjs/testing": "^9.3.10",
|
||||||
"build": "tsc -p .",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"dev": "tsc -p . --declaration -w",
|
"@types/bytes": "^3.1.1",
|
||||||
"typings": "tsc --declaration",
|
"@types/cors": "^2.8.13",
|
||||||
"docs": "npx typedoc src/index.ts",
|
"@types/cron": "^2.0.0",
|
||||||
"dev-test-watch": "mocha-typescript-watch"
|
"@types/crypto-js": "^4.1.1",
|
||||||
},
|
"@types/express": "^4.17.17",
|
||||||
"homepage": "https://git.osr-plastic.org/plastichub/lib-content",
|
"@types/jest": "^29.4.4",
|
||||||
"repository": {
|
"@types/lodash": "^4.14.191",
|
||||||
"type": "git",
|
"@types/morgan": "^1.9.4",
|
||||||
"url": "https://git.osr-plastic.org/plastichub/lib-content.git"
|
"@types/ms": "^0.7.31",
|
||||||
},
|
"@types/multer": "^1.4.7",
|
||||||
"engines": {
|
"@types/node": "^18.15.3",
|
||||||
"node": ">= 14.0.0"
|
"@types/passport-jwt": "^3.0.8",
|
||||||
},
|
"@types/supertest": "^2.0.12",
|
||||||
"license": "BSD-3-Clause",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"keywords": [
|
"@types/uuid": "^9.0.1",
|
||||||
"typescript"
|
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||||
]
|
"@typescript-eslint/parser": "^5.55.0",
|
||||||
|
"cspell": "^6.30.0",
|
||||||
|
"eslint": "^8.36.0",
|
||||||
|
"eslint-config-prettier": "^8.7.0",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.0.5",
|
||||||
|
"ts-loader": "^9.4.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"ts-prune": "^0.10.3",
|
||||||
|
"tsconfig-paths": "^4.1.2",
|
||||||
|
"typescript": "^5.0.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
scripts/docker/dockerfile.prod
Normal file
31
scripts/docker/dockerfile.prod
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Test Image
|
||||||
|
FROM node:lts-alpine as builder
|
||||||
|
LABEL maintainer "fedi.dayeg@gmail.com"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
|
||||||
|
RUN set -x && yarn --production=false
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Production Image
|
||||||
|
FROM node:lts-alpine as main
|
||||||
|
LABEL maintainer "fedi.dayeg@gmail.com"
|
||||||
|
|
||||||
|
ARG NODE_ENV=production
|
||||||
|
ENV NODE_ENV=${NODE_ENV}
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN touch .env
|
||||||
|
|
||||||
|
RUN set -x && yarn --production=true
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
CMD ["yarn", "start:prod"]
|
||||||
193
scripts/jenkinsfile
Normal file
193
scripts/jenkinsfile
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
def commit_id
|
||||||
|
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
options {
|
||||||
|
skipDefaultCheckout true
|
||||||
|
}
|
||||||
|
environment {
|
||||||
|
BASE_IMAGE='node:lts-alpine' // change this with same version of container image
|
||||||
|
|
||||||
|
APP_NAME = 'nest' // this will be container name
|
||||||
|
APP_NETWORK = 'app-network' // this network name
|
||||||
|
APP_PORT = 3000 // this project will serve in port 0.0.0.0:3000
|
||||||
|
APP_EXPOSE_PORT = 3000 // this project will expose at port 0.0.0.0:3000
|
||||||
|
|
||||||
|
NODE_JS = 'lts' // depends with our jenkins setting
|
||||||
|
|
||||||
|
HOST_IP = 'xx.xx.xx.xx' // this server ip for your production server
|
||||||
|
HOST_CREDENTIAL = '2637b88f-8dc8-4395-bd6b-0c6127720a89' // depends with our credentials jenkins
|
||||||
|
|
||||||
|
DOCKER_CREDENTIAL = 'ef108994-1241-4614-aab5-2aadd7a72284' // depends with our credentials jenkins
|
||||||
|
DOCKER_FILE= './prod/dockerfile'
|
||||||
|
DOCKER_USERNAME = 'nest' // docker hub username
|
||||||
|
DOCKER_REGISTRY = 'https://index.docker.io/v1/'
|
||||||
|
|
||||||
|
GIT = 'Default' // depends with our jenkins setting
|
||||||
|
GIT_BRANCH = 'main' // git branch
|
||||||
|
GIT_CREDENTIAL = '86535ad6-5d74-48c0-9852-bddbe1fbaff6' // depends with our credentials jenkins
|
||||||
|
GIT_URL = 'git@github.com:fedi-dayeg/nestjs-mongoose.git'
|
||||||
|
}
|
||||||
|
tools {
|
||||||
|
nodejs NODE_JS
|
||||||
|
git GIT
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Prepare') {
|
||||||
|
steps {
|
||||||
|
cleanWs()
|
||||||
|
checkout scm
|
||||||
|
|
||||||
|
sh 'node --version && npm --version && yarn --version'
|
||||||
|
sh 'docker --version'
|
||||||
|
sh 'docker ps -a'
|
||||||
|
|
||||||
|
script{
|
||||||
|
def nodeContainer = docker.image(BASE_IMAGE)
|
||||||
|
nodeContainer.pull()
|
||||||
|
nodeContainer.inside {
|
||||||
|
sh 'node --version'
|
||||||
|
sh 'npm --version'
|
||||||
|
sh 'yarn --version'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Clone') {
|
||||||
|
steps {
|
||||||
|
git branch: GIT_BRANCH,
|
||||||
|
credentialsId: GIT_CREDENTIAL,
|
||||||
|
url: GIT_URL
|
||||||
|
|
||||||
|
sh "git rev-parse --short HEAD > .git/commit-id"
|
||||||
|
sh "grep -o '\"version\": \"[^\"]*' package.json | grep -o '[^\"]*\$' > .git/version-id"
|
||||||
|
|
||||||
|
script{
|
||||||
|
commit_id = readFile('.git/commit-id').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Build'){
|
||||||
|
steps{
|
||||||
|
script{
|
||||||
|
def app_image = "${DOCKER_USERNAME}/${APP_NAME}-builder:${commit_id}"
|
||||||
|
docker.build(app_image, "--target builder -f ${DOCKER_FILE} .")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Unit Test') {
|
||||||
|
steps {
|
||||||
|
script{
|
||||||
|
def app_image = "${DOCKER_USERNAME}/${APP_NAME}-builder:${commit_id}"
|
||||||
|
def container = "${APP_NAME}-testing"
|
||||||
|
|
||||||
|
try{
|
||||||
|
sh "docker stop ${container} && docker rm ${container}"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
try{
|
||||||
|
sh "docker network create ${APP_NETWORK} --driver=bridge"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
sh "docker run --rm --network ${APP_NETWORK} \
|
||||||
|
--volume /app/${APP_NAME}/.env:/app/.env \
|
||||||
|
--name ${container} \
|
||||||
|
${app_image} \
|
||||||
|
sh -c 'yarn test:unit'"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Push') {
|
||||||
|
steps {
|
||||||
|
script{
|
||||||
|
def version_id = readFile('.git/version-id').trim()
|
||||||
|
def app_image = "${DOCKER_USERNAME}/${APP_NAME}:${commit_id}"
|
||||||
|
|
||||||
|
def app = docker.build(app_image, "--target main -f ${DOCKER_FILE} .")
|
||||||
|
docker.withRegistry(DOCKER_REGISTRY, DOCKER_CREDENTIAL) {
|
||||||
|
app.push('latest')
|
||||||
|
app.push("v${version_id}")
|
||||||
|
app.push("v${version_id}_sha-${commit_id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Deploy') {
|
||||||
|
steps {
|
||||||
|
script{
|
||||||
|
def version_id = readFile('.git/version-id').trim()
|
||||||
|
def app_image = "${DOCKER_USERNAME}/${APP_NAME}:v${version_id}_sha-${commit_id}"
|
||||||
|
|
||||||
|
def remote = [:]
|
||||||
|
remote.name = APP_NAME
|
||||||
|
remote.host = HOST_IP
|
||||||
|
remote.allowAnyHosts = true
|
||||||
|
withCredentials([sshUserPrivateKey(credentialsId: HOST_CREDENTIAL, keyFileVariable: 'IDENTITY', usernameVariable: 'USERNAME')]) {
|
||||||
|
|
||||||
|
remote.user = USERNAME
|
||||||
|
remote.identityFile = IDENTITY
|
||||||
|
|
||||||
|
try{
|
||||||
|
sshCommand remote: remote, command: "docker stop ${APP_NAME} && docker rm ${APP_NAME}"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
try{
|
||||||
|
sshCommand remote: remote, command: "docker network create ${APP_NETWORK} --driver=bridge"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
sshCommand remote: remote, command: "docker run -itd \
|
||||||
|
--hostname ${APP_NAME} \
|
||||||
|
--publish ${APP_EXPOSE_PORT}:${APP_PORT} \
|
||||||
|
--network ${APP_NETWORK} \
|
||||||
|
--volume /app/${APP_NAME}/logs/:/app/logs/ \
|
||||||
|
--volume /app/${APP_NAME}/.env:/app/.env \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--name ${APP_NAME} ${app_image}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Clean'){
|
||||||
|
steps {
|
||||||
|
script{
|
||||||
|
def remote = [:]
|
||||||
|
remote.name = APP_NAME
|
||||||
|
remote.host = HOST_IP
|
||||||
|
remote.allowAnyHosts = true
|
||||||
|
withCredentials([sshUserPrivateKey(credentialsId: HOST_CREDENTIAL, keyFileVariable: 'IDENTITY', usernameVariable: 'USERNAME')]) {
|
||||||
|
|
||||||
|
remote.user = USERNAME
|
||||||
|
remote.identityFile = IDENTITY
|
||||||
|
|
||||||
|
try{
|
||||||
|
sshCommand remote: remote, command: "docker container prune --force"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
try{
|
||||||
|
sshCommand remote: remote, command: "docker image prune --force"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
try{
|
||||||
|
sshCommand remote: remote, command: "docker rmi \$(docker images **/${APP_NAME}** -q) --force"
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
sh "docker container prune --force"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
try{
|
||||||
|
sh "docker image prune --force"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
try{
|
||||||
|
sh "docker rmi \$(docker images **/${APP_NAME}** -q) --force"
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/.gitignore
vendored
0
src/.gitignore
vendored
20
src/app/app.module.ts
Normal file
20
src/app/app.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JobsModule } from 'src/jobs/jobs.module';
|
||||||
|
import { AppController } from './controllers/app.controller';
|
||||||
|
import { RouterModule } from 'src/router/router.module';
|
||||||
|
import { CommonModule } from 'src/common/common.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
|
||||||
|
// Jobs
|
||||||
|
JobsModule.forRoot(),
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
RouterModule.forRoot(),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
1
src/app/constants/app.constant.ts
Normal file
1
src/app/constants/app.constant.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const APP_LANGUAGE = 'en';
|
||||||
5
src/app/constants/app.enum.constant.ts
Normal file
5
src/app/constants/app.enum.constant.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum ENUM_APP_ENVIRONMENT {
|
||||||
|
PRODUCTION = 'production',
|
||||||
|
STAGING = 'staging',
|
||||||
|
DEVELOPMENT = 'development',
|
||||||
|
}
|
||||||
81
src/app/controllers/app.controller.ts
Normal file
81
src/app/controllers/app.controller.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AppHelloApiKeyDoc, AppHelloDoc } from 'src/app/docs/app.doc';
|
||||||
|
import { AppHelloSerialization } from 'src/app/serializations/app.hello.serialization';
|
||||||
|
import { ApiKeyProtected } from 'src/common/api-key/decorators/api-key.decorator';
|
||||||
|
import { HelperDateService } from 'src/common/helper/services/helper.date.service';
|
||||||
|
import { ENUM_LOGGER_ACTION } from 'src/common/logger/constants/logger.enum.constant';
|
||||||
|
import { Logger } from 'src/common/logger/decorators/logger.decorator';
|
||||||
|
import { RequestUserAgent } from 'src/common/request/decorators/request.decorator';
|
||||||
|
import { Response } from 'src/common/response/decorators/response.decorator';
|
||||||
|
import { IResponse } from 'src/common/response/interfaces/response.interface';
|
||||||
|
import { IResult } from 'ua-parser-js';
|
||||||
|
|
||||||
|
@ApiTags('hello')
|
||||||
|
@Controller({
|
||||||
|
version: VERSION_NEUTRAL,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
export class AppController {
|
||||||
|
private readonly serviceName: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly helperDateService: HelperDateService
|
||||||
|
) {
|
||||||
|
this.serviceName = this.configService.get<string>('app.name');
|
||||||
|
}
|
||||||
|
|
||||||
|
@AppHelloDoc()
|
||||||
|
@Response('app.hello', { serialization: AppHelloSerialization })
|
||||||
|
@Logger(ENUM_LOGGER_ACTION.TEST, { tags: ['test'] })
|
||||||
|
@Get('/hello')
|
||||||
|
async hello(@RequestUserAgent() userAgent: IResult): Promise<IResponse> {
|
||||||
|
const newDate = this.helperDateService.create();
|
||||||
|
|
||||||
|
return {
|
||||||
|
_metadata: {
|
||||||
|
customProperty: {
|
||||||
|
messageProperties: {
|
||||||
|
serviceName: this.serviceName
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
userAgent,
|
||||||
|
date: newDate,
|
||||||
|
format: this.helperDateService.format(newDate),
|
||||||
|
timestamp: this.helperDateService.timestamp(newDate),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@AppHelloApiKeyDoc()
|
||||||
|
@Response('app.hello', { serialization: AppHelloSerialization })
|
||||||
|
@Logger(ENUM_LOGGER_ACTION.TEST, { tags: ['test'] })
|
||||||
|
@ApiKeyProtected()
|
||||||
|
@Get('/hello/api-key')
|
||||||
|
async helloApiKey(
|
||||||
|
@RequestUserAgent() userAgent: IResult
|
||||||
|
): Promise<IResponse> {
|
||||||
|
const newDate = this.helperDateService.create();
|
||||||
|
|
||||||
|
return {
|
||||||
|
_metadata: {
|
||||||
|
customProperty: {
|
||||||
|
messageProperties: {
|
||||||
|
serviceName: this.serviceName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
userAgent,
|
||||||
|
date: newDate,
|
||||||
|
format: this.helperDateService.format(newDate),
|
||||||
|
timestamp: this.helperDateService.timestamp(newDate),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app/docs/app.doc.ts
Normal file
30
src/app/docs/app.doc.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import { AppHelloSerialization } from 'src/app/serializations/app.hello.serialization';
|
||||||
|
import { Doc } from 'src/common/doc/decorators/doc.decorator';
|
||||||
|
|
||||||
|
export function AppHelloDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<AppHelloSerialization>('app.hello', {
|
||||||
|
response: {
|
||||||
|
serialization: AppHelloSerialization,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppHelloApiKeyDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<AppHelloSerialization>('app.helloApiKey', {
|
||||||
|
auth: {
|
||||||
|
apiKey: true,
|
||||||
|
},
|
||||||
|
requestHeader: {
|
||||||
|
timestamp: true,
|
||||||
|
userAgent: true,
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
serialization: AppHelloSerialization,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/app/serializations/app.hello.serialization.ts
Normal file
30
src/app/serializations/app.hello.serialization.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IResult } from 'ua-parser-js';
|
||||||
|
|
||||||
|
export class AppHelloSerialization {
|
||||||
|
@ApiProperty({
|
||||||
|
example: {
|
||||||
|
ua: 'PostmanRuntime/7.29.0',
|
||||||
|
browser: {},
|
||||||
|
engine: {},
|
||||||
|
os: {},
|
||||||
|
device: {},
|
||||||
|
cpu: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
readonly userAgent: IResult;
|
||||||
|
|
||||||
|
@ApiProperty({ example: faker.date.recent() })
|
||||||
|
@Type(() => String)
|
||||||
|
readonly date: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: faker.date.recent() })
|
||||||
|
readonly format: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 1660190937231,
|
||||||
|
})
|
||||||
|
readonly timestamp: number;
|
||||||
|
}
|
||||||
22
src/cli.ts
Normal file
22
src/cli.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { CommandModule, CommandService } from 'nestjs-command';
|
||||||
|
import { MigrationModule } from './migration/migration.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.createApplicationContext(MigrationModule, {
|
||||||
|
logger: ['error'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await app.select(CommandModule).get(CommandService).exec();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
logger.error(err, 'Migration');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
12
src/common/api-key/api-key.module.ts
Normal file
12
src/common/api-key/api-key.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ApiKeyXApiKeyStrategy } from 'src/common/api-key/guards/x-api-key/api-key.x-api-key.strategy';
|
||||||
|
import { ApiKeyRepositoryModule } from 'src/common/api-key/repository/api-key.repository.module';
|
||||||
|
import { ApiKeyService } from 'src/common/api-key/services/api-key.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [ApiKeyService, ApiKeyXApiKeyStrategy],
|
||||||
|
exports: [ApiKeyService],
|
||||||
|
controllers: [],
|
||||||
|
imports: [ApiKeyRepositoryModule],
|
||||||
|
})
|
||||||
|
export class ApiKeyModule {}
|
||||||
1
src/common/api-key/constants/api-key.constant.ts
Normal file
1
src/common/api-key/constants/api-key.constant.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_KEY_ACTIVE_META_KEY = 'ApiKeyActiveMetaKey';
|
||||||
22
src/common/api-key/constants/api-key.doc.ts
Normal file
22
src/common/api-key/constants/api-key.doc.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
|
||||||
|
export const ApiKeyDocQueryIsActive = [
|
||||||
|
{
|
||||||
|
name: 'isActive',
|
||||||
|
allowEmptyValue: false,
|
||||||
|
required: true,
|
||||||
|
type: 'string',
|
||||||
|
example: 'true,false',
|
||||||
|
description: "boolean value with ',' delimiter",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ApiKeyDocParamsGet = [
|
||||||
|
{
|
||||||
|
name: 'apiKey',
|
||||||
|
allowEmptyValue: false,
|
||||||
|
required: true,
|
||||||
|
type: 'string',
|
||||||
|
example: faker.datatype.uuid(),
|
||||||
|
},
|
||||||
|
];
|
||||||
9
src/common/api-key/constants/api-key.list.constant.ts
Normal file
9
src/common/api-key/constants/api-key.list.constant.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ENUM_PAGINATION_ORDER_DIRECTION_TYPE } from 'src/common/pagination/constants/pagination.enum.constant';
|
||||||
|
|
||||||
|
export const API_KEY_DEFAULT_PER_PAGE = 20;
|
||||||
|
export const API_KEY_DEFAULT_ORDER_BY = 'createdAt';
|
||||||
|
export const API_KEY_DEFAULT_ORDER_DIRECTION =
|
||||||
|
ENUM_PAGINATION_ORDER_DIRECTION_TYPE.ASC;
|
||||||
|
export const API_KEY_DEFAULT_AVAILABLE_ORDER_BY = ['name', 'key', 'createdAt'];
|
||||||
|
export const API_KEY_DEFAULT_AVAILABLE_SEARCH = ['name', 'key'];
|
||||||
|
export const API_KEY_DEFAULT_IS_ACTIVE = [true, false];
|
||||||
10
src/common/api-key/constants/api-key.status-code.constant.ts
Normal file
10
src/common/api-key/constants/api-key.status-code.constant.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export enum ENUM_API_KEY_STATUS_CODE_ERROR {
|
||||||
|
API_KEY_NEEDED_ERROR = 5120,
|
||||||
|
API_KEY_NOT_FOUND_ERROR = 5121,
|
||||||
|
API_KEY_IS_ACTIVE_ERROR = 5122,
|
||||||
|
API_KEY_EXPIRED_ERROR = 5123,
|
||||||
|
API_KEY_INACTIVE_ERROR = 5124,
|
||||||
|
API_KEY_INVALID_ERROR = 5125,
|
||||||
|
API_KEY_WRONG_ERROR = 5126,
|
||||||
|
API_KEY_EXIST_ERROR = 5127,
|
||||||
|
}
|
||||||
118
src/common/api-key/controllers/api-key.admin.controller.ts
Normal file
118
src/common/api-key/controllers/api-key.admin.controller.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Patch,
|
||||||
|
Put,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
ApiKeyUpdateActiveGuard,
|
||||||
|
ApiKeyUpdateGuard,
|
||||||
|
ApiKeyUpdateInactiveGuard,
|
||||||
|
} from 'src/common/api-key/decorators/api-key.admin.decorator';
|
||||||
|
import { GetApiKey } from 'src/common/api-key/decorators/api-key.decorator';
|
||||||
|
import {
|
||||||
|
ApiKeyActiveDoc,
|
||||||
|
ApiKeyInactiveDoc,
|
||||||
|
ApiKeyUpdateDoc,
|
||||||
|
} from 'src/common/api-key/docs/api-key.admin.doc';
|
||||||
|
import { ApiKeyRequestDto } from 'src/common/api-key/dtos/api-key.request.dto';
|
||||||
|
import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto';
|
||||||
|
import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import { ApiKeyService } from 'src/common/api-key/services/api-key.service';
|
||||||
|
import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant';
|
||||||
|
import { AuthJwtAccessProtected } from 'src/common/auth/decorators/auth.jwt.decorator';
|
||||||
|
import { AuthPermissionProtected } from 'src/common/auth/decorators/auth.permission.decorator';
|
||||||
|
import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant';
|
||||||
|
import { RequestParamGuard } from 'src/common/request/decorators/request.decorator';
|
||||||
|
import { Response } from 'src/common/response/decorators/response.decorator';
|
||||||
|
import { IResponse } from 'src/common/response/interfaces/response.interface';
|
||||||
|
import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization';
|
||||||
|
|
||||||
|
@ApiTags('admin.apiKey')
|
||||||
|
@Controller({
|
||||||
|
version: '1',
|
||||||
|
path: '/api-key',
|
||||||
|
})
|
||||||
|
export class ApiKeyAdminController {
|
||||||
|
constructor(private readonly apiKeyService: ApiKeyService) {}
|
||||||
|
|
||||||
|
@ApiKeyInactiveDoc()
|
||||||
|
@Response('apiKey.inactive')
|
||||||
|
@ApiKeyUpdateInactiveGuard()
|
||||||
|
@RequestParamGuard(ApiKeyRequestDto)
|
||||||
|
@AuthPermissionProtected(
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_READ,
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE,
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_INACTIVE
|
||||||
|
)
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Patch('/update/:apiKey/inactive')
|
||||||
|
async inactive(@GetApiKey() apiKey: ApiKeyDoc): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.apiKeyService.inactive(apiKey);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new InternalServerErrorException({
|
||||||
|
statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN,
|
||||||
|
message: 'http.serverError.internalServerError',
|
||||||
|
_error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiKeyActiveDoc()
|
||||||
|
@Response('apiKey.active')
|
||||||
|
@ApiKeyUpdateActiveGuard()
|
||||||
|
@RequestParamGuard(ApiKeyRequestDto)
|
||||||
|
@AuthPermissionProtected(
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_READ,
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE,
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_ACTIVE
|
||||||
|
)
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Patch('/update/:apiKey/active')
|
||||||
|
async active(@GetApiKey() apiKey: ApiKeyDoc): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.apiKeyService.active(apiKey);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new InternalServerErrorException({
|
||||||
|
statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN,
|
||||||
|
message: 'http.serverError.internalServerError',
|
||||||
|
_error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiKeyUpdateDoc()
|
||||||
|
@Response('apiKey.updateDate', { serialization: ResponseIdSerialization })
|
||||||
|
@ApiKeyUpdateGuard()
|
||||||
|
@RequestParamGuard(ApiKeyRequestDto)
|
||||||
|
@AuthPermissionProtected(
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_READ,
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE,
|
||||||
|
ENUM_AUTH_PERMISSIONS.API_KEY_UPDATE_DATE
|
||||||
|
)
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Put('/update/:apiKey/date')
|
||||||
|
async updateDate(
|
||||||
|
@Body() body: ApiKeyUpdateDateDto,
|
||||||
|
@GetApiKey() apiKey: ApiKeyDoc
|
||||||
|
): Promise<IResponse> {
|
||||||
|
try {
|
||||||
|
await this.apiKeyService.updateDate(apiKey, body);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new InternalServerErrorException({
|
||||||
|
statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN,
|
||||||
|
message: 'http.serverError.internalServerError',
|
||||||
|
_error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: { _id: apiKey._id } };
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/common/api-key/controllers/api-key.controller.ts
Normal file
222
src/common/api-key/controllers/api-key.controller.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
ConflictException,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
API_KEY_DEFAULT_AVAILABLE_ORDER_BY,
|
||||||
|
API_KEY_DEFAULT_AVAILABLE_SEARCH,
|
||||||
|
API_KEY_DEFAULT_IS_ACTIVE,
|
||||||
|
API_KEY_DEFAULT_ORDER_BY,
|
||||||
|
API_KEY_DEFAULT_ORDER_DIRECTION,
|
||||||
|
API_KEY_DEFAULT_PER_PAGE,
|
||||||
|
} from 'src/common/api-key/constants/api-key.list.constant';
|
||||||
|
import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant';
|
||||||
|
import {
|
||||||
|
ApiKeyGetGuard,
|
||||||
|
ApiKeyUpdateGuard,
|
||||||
|
ApiKeyUpdateResetGuard,
|
||||||
|
} from 'src/common/api-key/decorators/api-key.admin.decorator';
|
||||||
|
import { GetApiKey } from 'src/common/api-key/decorators/api-key.decorator';
|
||||||
|
import {
|
||||||
|
ApiKeyCreateDoc,
|
||||||
|
ApiKeyGetDoc,
|
||||||
|
ApiKeyListDoc,
|
||||||
|
ApiKeyResetDoc,
|
||||||
|
ApiKeyUpdateDoc,
|
||||||
|
} from 'src/common/api-key/docs/api-key.admin.doc';
|
||||||
|
import { ApiKeyCreateDto } from 'src/common/api-key/dtos/api-key.create.dto';
|
||||||
|
import { ApiKeyRequestDto } from 'src/common/api-key/dtos/api-key.request.dto';
|
||||||
|
import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto';
|
||||||
|
import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface';
|
||||||
|
import {
|
||||||
|
ApiKeyDoc,
|
||||||
|
ApiKeyEntity,
|
||||||
|
} from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization';
|
||||||
|
import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization';
|
||||||
|
import { ApiKeyListSerialization } from 'src/common/api-key/serializations/api-key.list.serialization';
|
||||||
|
import { ApiKeyResetSerialization } from 'src/common/api-key/serializations/api-key.reset.serialization';
|
||||||
|
import { ApiKeyService } from 'src/common/api-key/services/api-key.service';
|
||||||
|
import {
|
||||||
|
AuthJwtAccessProtected,
|
||||||
|
AuthJwtPayload,
|
||||||
|
} from 'src/common/auth/decorators/auth.jwt.decorator';
|
||||||
|
import { ENUM_ERROR_STATUS_CODE_ERROR } from 'src/common/error/constants/error.status-code.constant';
|
||||||
|
import {
|
||||||
|
PaginationQuery,
|
||||||
|
PaginationQueryFilterInBoolean,
|
||||||
|
} from 'src/common/pagination/decorators/pagination.decorator';
|
||||||
|
import { PaginationListDto } from 'src/common/pagination/dtos/pagination.list.dto';
|
||||||
|
import { PaginationService } from 'src/common/pagination/services/pagination.service';
|
||||||
|
import { RequestParamGuard } from 'src/common/request/decorators/request.decorator';
|
||||||
|
import {
|
||||||
|
Response,
|
||||||
|
ResponsePaging,
|
||||||
|
} from 'src/common/response/decorators/response.decorator';
|
||||||
|
import {
|
||||||
|
IResponse,
|
||||||
|
IResponsePaging,
|
||||||
|
} from 'src/common/response/interfaces/response.interface';
|
||||||
|
import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization';
|
||||||
|
|
||||||
|
@ApiTags('apiKey')
|
||||||
|
@Controller({
|
||||||
|
version: '1',
|
||||||
|
path: '/api-key',
|
||||||
|
})
|
||||||
|
export class ApiKeyController {
|
||||||
|
constructor(
|
||||||
|
private readonly apiKeyService: ApiKeyService,
|
||||||
|
private readonly paginationService: PaginationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@ApiKeyListDoc()
|
||||||
|
@ResponsePaging('apiKey.list', {
|
||||||
|
serialization: ApiKeyListSerialization,
|
||||||
|
})
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Get('/list')
|
||||||
|
async list(
|
||||||
|
@PaginationQuery(
|
||||||
|
API_KEY_DEFAULT_PER_PAGE,
|
||||||
|
API_KEY_DEFAULT_ORDER_BY,
|
||||||
|
API_KEY_DEFAULT_ORDER_DIRECTION,
|
||||||
|
API_KEY_DEFAULT_AVAILABLE_SEARCH,
|
||||||
|
API_KEY_DEFAULT_AVAILABLE_ORDER_BY
|
||||||
|
)
|
||||||
|
{ _search, _limit, _offset, _order }: PaginationListDto,
|
||||||
|
@PaginationQueryFilterInBoolean('isActive', API_KEY_DEFAULT_IS_ACTIVE)
|
||||||
|
isActive: Record<string, any>
|
||||||
|
): Promise<IResponsePaging> {
|
||||||
|
const find: Record<string, any> = {
|
||||||
|
..._search,
|
||||||
|
...isActive,
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiKeys: ApiKeyEntity[] = await this.apiKeyService.findAll(find, {
|
||||||
|
paging: {
|
||||||
|
limit: _limit,
|
||||||
|
offset: _offset,
|
||||||
|
},
|
||||||
|
order: _order,
|
||||||
|
});
|
||||||
|
const total: number = await this.apiKeyService.getTotal(find);
|
||||||
|
const totalPage: number = this.paginationService.totalPage(
|
||||||
|
total,
|
||||||
|
_limit
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
_pagination: { totalPage, total },
|
||||||
|
data: apiKeys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiKeyGetDoc()
|
||||||
|
@Response('apiKey.get', {
|
||||||
|
serialization: ApiKeyGetSerialization,
|
||||||
|
})
|
||||||
|
@ApiKeyGetGuard()
|
||||||
|
@RequestParamGuard(ApiKeyRequestDto)
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Get('get/:apiKey')
|
||||||
|
async get(@GetApiKey(true) apiKey: ApiKeyEntity): Promise<IResponse> {
|
||||||
|
return { data: apiKey };
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiKeyCreateDoc()
|
||||||
|
@Response('apiKey.create', { serialization: ApiKeyCreateSerialization })
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Post('/create')
|
||||||
|
async create(
|
||||||
|
@AuthJwtPayload('_id') _id: string,
|
||||||
|
@Body() body: ApiKeyCreateDto
|
||||||
|
): Promise<IResponse> {
|
||||||
|
const checkUser: boolean = await this.apiKeyService.existByUser(_id);
|
||||||
|
if (checkUser) {
|
||||||
|
throw new ConflictException({
|
||||||
|
statusCode: ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXIST_ERROR,
|
||||||
|
message: 'apiKey.error.exist',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created: IApiKeyCreated = await this.apiKeyService.create(
|
||||||
|
_id,
|
||||||
|
body
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
_id: created.doc._id,
|
||||||
|
secret: created.secret,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new InternalServerErrorException({
|
||||||
|
statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN,
|
||||||
|
message: 'http.serverError.internalServerError',
|
||||||
|
_error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiKeyResetDoc()
|
||||||
|
@Response('apiKey.reset', { serialization: ApiKeyResetSerialization })
|
||||||
|
@ApiKeyUpdateResetGuard()
|
||||||
|
@RequestParamGuard(ApiKeyRequestDto)
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Patch('/update/:apiKey/reset')
|
||||||
|
async reset(@GetApiKey() apiKey: ApiKeyDoc): Promise<IResponse> {
|
||||||
|
try {
|
||||||
|
const secret: string = await this.apiKeyService.createSecret();
|
||||||
|
const updated: ApiKeyDoc = await this.apiKeyService.reset(
|
||||||
|
apiKey,
|
||||||
|
secret
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
_id: updated._id,
|
||||||
|
secret,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new InternalServerErrorException({
|
||||||
|
statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN,
|
||||||
|
message: 'http.serverError.internalServerError',
|
||||||
|
_error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiKeyUpdateDoc()
|
||||||
|
@Response('apiKey.update', { serialization: ResponseIdSerialization })
|
||||||
|
@ApiKeyUpdateGuard()
|
||||||
|
@RequestParamGuard(ApiKeyRequestDto)
|
||||||
|
@AuthJwtAccessProtected()
|
||||||
|
@Put('/update/:apiKey')
|
||||||
|
async updateName(
|
||||||
|
@Body() body: ApiKeyUpdateDto,
|
||||||
|
@GetApiKey() apiKey: ApiKeyDoc
|
||||||
|
): Promise<IResponse> {
|
||||||
|
try {
|
||||||
|
await this.apiKeyService.update(apiKey, body);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new InternalServerErrorException({
|
||||||
|
statusCode: ENUM_ERROR_STATUS_CODE_ERROR.ERROR_UNKNOWN,
|
||||||
|
message: 'http.serverError.internalServerError',
|
||||||
|
_error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: { _id: apiKey._id } };
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/common/api-key/decorators/api-key.admin.decorator.ts
Normal file
60
src/common/api-key/decorators/api-key.admin.decorator.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';
|
||||||
|
import { API_KEY_ACTIVE_META_KEY } from 'src/common/api-key/constants/api-key.constant';
|
||||||
|
import { ApiKeyActiveGuard } from 'src/common/api-key/guards/api-key.active.guard';
|
||||||
|
import { ApiKeyExpiredGuard } from 'src/common/api-key/guards/api-key.expired.guard';
|
||||||
|
import { ApiKeyNotFoundGuard } from 'src/common/api-key/guards/api-key.not-found.guard';
|
||||||
|
import { ApiKeyPutToRequestGuard } from 'src/common/api-key/guards/api-key.put-to-request.guard';
|
||||||
|
|
||||||
|
export function ApiKeyGetGuard(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(ApiKeyPutToRequestGuard, ApiKeyNotFoundGuard)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyUpdateGuard(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(
|
||||||
|
ApiKeyPutToRequestGuard,
|
||||||
|
ApiKeyNotFoundGuard,
|
||||||
|
ApiKeyActiveGuard,
|
||||||
|
ApiKeyExpiredGuard
|
||||||
|
),
|
||||||
|
SetMetadata(API_KEY_ACTIVE_META_KEY, [true])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyUpdateResetGuard(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(
|
||||||
|
ApiKeyPutToRequestGuard,
|
||||||
|
ApiKeyNotFoundGuard,
|
||||||
|
ApiKeyActiveGuard,
|
||||||
|
ApiKeyExpiredGuard
|
||||||
|
),
|
||||||
|
SetMetadata(API_KEY_ACTIVE_META_KEY, [true])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyUpdateActiveGuard(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(
|
||||||
|
ApiKeyPutToRequestGuard,
|
||||||
|
ApiKeyNotFoundGuard,
|
||||||
|
ApiKeyActiveGuard,
|
||||||
|
ApiKeyExpiredGuard
|
||||||
|
),
|
||||||
|
SetMetadata(API_KEY_ACTIVE_META_KEY, [false])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyUpdateInactiveGuard(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(
|
||||||
|
ApiKeyPutToRequestGuard,
|
||||||
|
ApiKeyNotFoundGuard,
|
||||||
|
ApiKeyActiveGuard,
|
||||||
|
ApiKeyExpiredGuard
|
||||||
|
),
|
||||||
|
SetMetadata(API_KEY_ACTIVE_META_KEY, [true])
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/common/api-key/decorators/api-key.decorator.ts
Normal file
27
src/common/api-key/decorators/api-key.decorator.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
applyDecorators,
|
||||||
|
createParamDecorator,
|
||||||
|
ExecutionContext,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiKeyXApiKeyGuard } from 'src/common/api-key/guards/x-api-key/api-key.x-api-key.guard';
|
||||||
|
import { IApiKeyPayload } from 'src/common/api-key/interfaces/api-key.interface';
|
||||||
|
import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
|
||||||
|
export const ApiKeyPayload: () => ParameterDecorator = createParamDecorator(
|
||||||
|
(data: string, ctx: ExecutionContext): IApiKeyPayload => {
|
||||||
|
const { apiKey } = ctx.switchToHttp().getRequest();
|
||||||
|
return data ? apiKey[data] : apiKey;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function ApiKeyProtected(): MethodDecorator {
|
||||||
|
return applyDecorators(UseGuards(ApiKeyXApiKeyGuard));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetApiKey = createParamDecorator(
|
||||||
|
(returnPlain: boolean, ctx: ExecutionContext): ApiKeyDoc => {
|
||||||
|
const { __apiKey } = ctx.switchToHttp().getRequest();
|
||||||
|
return returnPlain ? __apiKey.toObject() : __apiKey;
|
||||||
|
}
|
||||||
|
);
|
||||||
119
src/common/api-key/docs/api-key.admin.doc.ts
Normal file
119
src/common/api-key/docs/api-key.admin.doc.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { applyDecorators, HttpStatus } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiKeyDocParamsGet,
|
||||||
|
ApiKeyDocQueryIsActive,
|
||||||
|
} from 'src/common/api-key/constants/api-key.doc';
|
||||||
|
import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization';
|
||||||
|
import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization';
|
||||||
|
import { ApiKeyListSerialization } from 'src/common/api-key/serializations/api-key.list.serialization';
|
||||||
|
import { Doc, DocPaging } from 'src/common/doc/decorators/doc.decorator';
|
||||||
|
import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization';
|
||||||
|
|
||||||
|
export function ApiKeyListDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
DocPaging<ApiKeyListSerialization>('apiKey.list', {
|
||||||
|
auth: {
|
||||||
|
jwtAccessToken: true,
|
||||||
|
permissionToken: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
queries: ApiKeyDocQueryIsActive,
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
serialization: ApiKeyListSerialization,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyGetDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<ApiKeyGetSerialization>('apiKey.get', {
|
||||||
|
auth: {
|
||||||
|
jwtAccessToken: true,
|
||||||
|
permissionToken: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
params: ApiKeyDocParamsGet,
|
||||||
|
},
|
||||||
|
response: { serialization: ApiKeyGetSerialization },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyCreateDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<ApiKeyCreateSerialization>('apiKey.create', {
|
||||||
|
auth: {
|
||||||
|
jwtAccessToken: true,
|
||||||
|
permissionToken: true,
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
httpStatus: HttpStatus.CREATED,
|
||||||
|
serialization: ApiKeyCreateSerialization,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyActiveDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<void>('apiKey.active', {
|
||||||
|
auth: {
|
||||||
|
jwtAccessToken: true,
|
||||||
|
permissionToken: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
params: ApiKeyDocParamsGet,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyInactiveDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<void>('apiKey.inactive', {
|
||||||
|
auth: {
|
||||||
|
jwtAccessToken: true,
|
||||||
|
permissionToken: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
params: ApiKeyDocParamsGet,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyResetDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<void>('apiKey.reset', {
|
||||||
|
auth: {
|
||||||
|
jwtAccessToken: true,
|
||||||
|
permissionToken: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
params: ApiKeyDocParamsGet,
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
serialization: ApiKeyCreateSerialization,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyUpdateDoc(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
Doc<ResponseIdSerialization>('apiKey.update', {
|
||||||
|
auth: {
|
||||||
|
jwtAccessToken: true,
|
||||||
|
permissionToken: true,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
params: ApiKeyDocParamsGet,
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
serialization: ResponseIdSerialization,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/common/api-key/dtos/api-key.active.dto.ts
Normal file
15
src/common/api-key/dtos/api-key.active.dto.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsNotEmpty, IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class ApiKeyActiveDto {
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'isActive',
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsBoolean()
|
||||||
|
@IsNotEmpty()
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
57
src/common/api-key/dtos/api-key.create.dto.ts
Normal file
57
src/common/api-key/dtos/api-key.create.dto.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { ApiProperty, PartialType } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
MaxLength,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto';
|
||||||
|
|
||||||
|
export class ApiKeyCreateDto extends PartialType(ApiKeyUpdateDateDto) {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Api Key name',
|
||||||
|
example: `testapiname`,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(50)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Description of api key',
|
||||||
|
example: 'blabla description',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(100)
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiKeyCreateRawDto extends ApiKeyCreateDto {
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'key',
|
||||||
|
example: faker.random.alphaNumeric(10),
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(50)
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'secret',
|
||||||
|
example: faker.random.alphaNumeric(20),
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
secret: string;
|
||||||
|
}
|
||||||
16
src/common/api-key/dtos/api-key.request.dto.ts
Normal file
16
src/common/api-key/dtos/api-key.request.dto.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsNotEmpty, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
export class ApiKeyRequestDto {
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'apiKey',
|
||||||
|
description: 'apiKey id',
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID('4')
|
||||||
|
@Type(() => String)
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
32
src/common/api-key/dtos/api-key.update-date.dto.ts
Normal file
32
src/common/api-key/dtos/api-key.update-date.dto.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { IsDate, IsNotEmpty } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { MinGreaterThanEqual } from 'src/common/request/validations/request.min-greater-than-equal.validation';
|
||||||
|
import { MinDateToday } from 'src/common/request/validations/request.min-date-today.validation';
|
||||||
|
|
||||||
|
export class ApiKeyUpdateDateDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Api Key start date',
|
||||||
|
example: faker.date.recent(),
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Type(() => Date)
|
||||||
|
@IsDate()
|
||||||
|
@MinDateToday()
|
||||||
|
startDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Api Key end date',
|
||||||
|
example: faker.date.recent(),
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Type(() => Date)
|
||||||
|
@IsDate()
|
||||||
|
@MinGreaterThanEqual('startDate')
|
||||||
|
endDate: Date;
|
||||||
|
}
|
||||||
7
src/common/api-key/dtos/api-key.update.dto.ts
Normal file
7
src/common/api-key/dtos/api-key.update.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { PickType } from '@nestjs/swagger';
|
||||||
|
import { ApiKeyCreateDto } from './api-key.create.dto';
|
||||||
|
|
||||||
|
export class ApiKeyUpdateDto extends PickType(ApiKeyCreateDto, [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
] as const) {}
|
||||||
36
src/common/api-key/guards/api-key.active.guard.ts
Normal file
36
src/common/api-key/guards/api-key.active.guard.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { API_KEY_ACTIVE_META_KEY } from 'src/common/api-key/constants/api-key.constant';
|
||||||
|
import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyActiveGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const required: boolean[] = this.reflector.getAllAndOverride<boolean[]>(
|
||||||
|
API_KEY_ACTIVE_META_KEY,
|
||||||
|
[context.getHandler(), context.getClass()]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!required) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { __apiKey } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
if (!required.includes(__apiKey.isActive)) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR,
|
||||||
|
message: 'apiKey.error.isActiveInvalid',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/common/api-key/guards/api-key.expired.guard.ts
Normal file
31
src/common/api-key/guards/api-key.expired.guard.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant';
|
||||||
|
import { HelperDateService } from 'src/common/helper/services/helper.date.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyExpiredGuard implements CanActivate {
|
||||||
|
constructor(private readonly helperDateService: HelperDateService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const { __apiKey } = context.switchToHttp().getRequest();
|
||||||
|
const today: Date = this.helperDateService.create();
|
||||||
|
|
||||||
|
if (
|
||||||
|
__apiKey.startDate &&
|
||||||
|
__apiKey.endDate &&
|
||||||
|
today > __apiKey.endDate
|
||||||
|
) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR,
|
||||||
|
message: 'apiKey.error.expired',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/common/api-key/guards/api-key.not-found.guard.ts
Normal file
23
src/common/api-key/guards/api-key.not-found.guard.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyNotFoundGuard implements CanActivate {
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const { __apiKey } = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
if (!__apiKey) {
|
||||||
|
throw new NotFoundException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR,
|
||||||
|
message: 'apiKey.error.notFound',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/common/api-key/guards/api-key.put-to-request.guard.ts
Normal file
19
src/common/api-key/guards/api-key.put-to-request.guard.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { ApiKeyDoc } from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import { ApiKeyService } from 'src/common/api-key/services/api-key.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyPutToRequestGuard implements CanActivate {
|
||||||
|
constructor(private readonly apiKeyService: ApiKeyService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const { params } = request;
|
||||||
|
const { apiKey } = params;
|
||||||
|
|
||||||
|
const check: ApiKeyDoc = await this.apiKeyService.findOneById(apiKey);
|
||||||
|
request.__apiKey = check;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import {
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { HelperNumberService } from 'src/common/helper/services/helper.number.service';
|
||||||
|
import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant';
|
||||||
|
import { BadRequestError } from 'passport-headerapikey';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyXApiKeyGuard extends AuthGuard('api-key') {
|
||||||
|
constructor(private readonly helperNumberService: HelperNumberService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest<IApiKeyPayload = any>(
|
||||||
|
err: Record<string, any>,
|
||||||
|
apiKey: IApiKeyPayload,
|
||||||
|
info: Error | string
|
||||||
|
): IApiKeyPayload {
|
||||||
|
if (err || !apiKey) {
|
||||||
|
if (
|
||||||
|
info instanceof BadRequestError &&
|
||||||
|
info.message === 'Missing API Key'
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NEEDED_ERROR,
|
||||||
|
message: 'apiKey.error.keyNeeded',
|
||||||
|
});
|
||||||
|
} else if (err) {
|
||||||
|
const statusCode: number = this.helperNumberService.create(
|
||||||
|
err.message as string
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
statusCode ===
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
statusCode,
|
||||||
|
message: 'apiKey.error.notFound',
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
statusCode ===
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
statusCode,
|
||||||
|
message: 'apiKey.error.inactive',
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
statusCode ===
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
statusCode,
|
||||||
|
message: 'apiKey.error.expired',
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
statusCode ===
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_WRONG_ERROR
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
statusCode,
|
||||||
|
message: 'apiKey.error.wrong',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR,
|
||||||
|
message: 'apiKey.error.invalid',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import Strategy from 'passport-headerapikey';
|
||||||
|
import { ENUM_API_KEY_STATUS_CODE_ERROR } from 'src/common/api-key/constants/api-key.status-code.constant';
|
||||||
|
import { ApiKeyEntity } from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import { ApiKeyService } from 'src/common/api-key/services/api-key.service';
|
||||||
|
import { HelperDateService } from 'src/common/helper/services/helper.date.service';
|
||||||
|
import { IRequestApp } from 'src/common/request/interfaces/request.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyXApiKeyStrategy extends PassportStrategy(
|
||||||
|
Strategy,
|
||||||
|
'api-key'
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
private readonly apiKeyService: ApiKeyService,
|
||||||
|
private readonly helperDateService: HelperDateService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
{ header: 'X-API-KEY', prefix: '' },
|
||||||
|
true,
|
||||||
|
async (
|
||||||
|
apiKey: string,
|
||||||
|
verified: (
|
||||||
|
error: Error,
|
||||||
|
user?: Record<string, any>,
|
||||||
|
info?: string | number
|
||||||
|
) => Promise<void>,
|
||||||
|
req: IRequestApp
|
||||||
|
) => this.validate(apiKey, verified, req)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(
|
||||||
|
apiKey: string,
|
||||||
|
verified: (
|
||||||
|
error: Error,
|
||||||
|
user?: ApiKeyEntity,
|
||||||
|
info?: string | number
|
||||||
|
) => Promise<void>,
|
||||||
|
req: IRequestApp
|
||||||
|
): Promise<void> {
|
||||||
|
const xApiKey: string[] = apiKey.split(':');
|
||||||
|
if (xApiKey.length !== 2) {
|
||||||
|
verified(
|
||||||
|
new Error(
|
||||||
|
`${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR}`
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = xApiKey[0];
|
||||||
|
const secret = xApiKey[1];
|
||||||
|
const today = this.helperDateService.create();
|
||||||
|
const authApi: ApiKeyEntity =
|
||||||
|
await this.apiKeyService.findOneByActiveKey(key);
|
||||||
|
|
||||||
|
if (!authApi) {
|
||||||
|
verified(
|
||||||
|
new Error(
|
||||||
|
`${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_NOT_FOUND_ERROR}`
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (!authApi.isActive) {
|
||||||
|
verified(
|
||||||
|
new Error(
|
||||||
|
`${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_IS_ACTIVE_ERROR}`
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
authApi.startDate &&
|
||||||
|
authApi.endDate &&
|
||||||
|
(authApi.startDate < today || authApi.endDate > today)
|
||||||
|
) {
|
||||||
|
verified(
|
||||||
|
new Error(
|
||||||
|
`${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_EXPIRED_ERROR}`
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateApiKey: boolean =
|
||||||
|
await this.apiKeyService.validateHashApiKey(secret, authApi.hash);
|
||||||
|
if (!validateApiKey) {
|
||||||
|
verified(
|
||||||
|
new Error(
|
||||||
|
`${ENUM_API_KEY_STATUS_CODE_ERROR.API_KEY_INVALID_ERROR}`
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.apiKey = {
|
||||||
|
_id: `${authApi._id}`,
|
||||||
|
key: authApi.key,
|
||||||
|
name: authApi.name,
|
||||||
|
};
|
||||||
|
verified(null, authApi);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/common/api-key/interfaces/api-key.interface.ts
Normal file
12
src/common/api-key/interfaces/api-key.interface.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiKeyDoc } from "src/common/api-key/repository/entities/api-key.entity";
|
||||||
|
|
||||||
|
export interface IApiKeyPayload {
|
||||||
|
_id: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApiKeyCreated {
|
||||||
|
secret: string;
|
||||||
|
doc: ApiKeyDoc;
|
||||||
|
}
|
||||||
92
src/common/api-key/interfaces/api-key.service.interface.ts
Normal file
92
src/common/api-key/interfaces/api-key.service.interface.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
ApiKeyCreateDto,
|
||||||
|
ApiKeyCreateRawDto,
|
||||||
|
} from 'src/common/api-key/dtos/api-key.create.dto';
|
||||||
|
import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto';
|
||||||
|
import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto';
|
||||||
|
import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface';
|
||||||
|
import {
|
||||||
|
ApiKeyDoc,
|
||||||
|
ApiKeyEntity,
|
||||||
|
} from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import {
|
||||||
|
IDatabaseCreateOptions,
|
||||||
|
IDatabaseFindAllOptions,
|
||||||
|
IDatabaseFindOneOptions,
|
||||||
|
IDatabaseManyOptions,
|
||||||
|
IDatabaseOptions,
|
||||||
|
} from 'src/common/database/interfaces/database.interface';
|
||||||
|
|
||||||
|
export interface IApiKeyService {
|
||||||
|
findAll(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions
|
||||||
|
): Promise<ApiKeyEntity[]>;
|
||||||
|
|
||||||
|
findOneById(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
findOne(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
findOneByKey(
|
||||||
|
key: string,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
findOneByActiveKey(
|
||||||
|
key: string,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
getTotal(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseOptions
|
||||||
|
): Promise<number>;
|
||||||
|
|
||||||
|
create(
|
||||||
|
user: string,
|
||||||
|
{name, description, startDate, endDate}: ApiKeyCreateDto,
|
||||||
|
options?: IDatabaseCreateOptions
|
||||||
|
): Promise<IApiKeyCreated>;
|
||||||
|
|
||||||
|
createRaw(
|
||||||
|
user: string,
|
||||||
|
{name, description, key, secret, startDate, endDate}: ApiKeyCreateRawDto,
|
||||||
|
options?: IDatabaseCreateOptions
|
||||||
|
): Promise<IApiKeyCreated>;
|
||||||
|
|
||||||
|
active(repository: ApiKeyDoc): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
inactive(repository: ApiKeyDoc): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
update(repository: ApiKeyDoc, data: ApiKeyUpdateDto): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
updateDate(
|
||||||
|
repository: ApiKeyDoc,
|
||||||
|
{startDate, endDate}: ApiKeyUpdateDateDto
|
||||||
|
): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
reset(repository: ApiKeyDoc, secret: string): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
delete(repository: ApiKeyDoc): Promise<ApiKeyDoc>;
|
||||||
|
|
||||||
|
validateHashApiKey(hashFromRequest: string, hash: string): Promise<boolean>;
|
||||||
|
|
||||||
|
createKey(): Promise<string>;
|
||||||
|
|
||||||
|
createSecret(): Promise<string>;
|
||||||
|
|
||||||
|
createHashApiKey(key: string, secret: string): Promise<string>;
|
||||||
|
|
||||||
|
deleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseManyOptions
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
inactiveManyByEndDate(options?: IDatabaseManyOptions): Promise<boolean>;
|
||||||
|
}
|
||||||
26
src/common/api-key/repository/api-key.repository.module.ts
Normal file
26
src/common/api-key/repository/api-key.repository.module.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import {
|
||||||
|
ApiKeyEntity,
|
||||||
|
ApiKeySchema,
|
||||||
|
} from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import { ApiKeyRepository } from 'src/common/api-key/repository/repositories/api-key.repository';
|
||||||
|
import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [ApiKeyRepository],
|
||||||
|
exports: [ApiKeyRepository],
|
||||||
|
controllers: [],
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forFeature(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: ApiKeyEntity.name,
|
||||||
|
schema: ApiKeySchema,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
DATABASE_CONNECTION_NAME
|
||||||
|
),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ApiKeyRepositoryModule {}
|
||||||
85
src/common/api-key/repository/entities/api-key.entity.ts
Normal file
85
src/common/api-key/repository/entities/api-key.entity.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Prop, SchemaFactory } from '@nestjs/mongoose';
|
||||||
|
import { CallbackWithoutResultAndOptionalError } from 'mongoose';
|
||||||
|
import { DatabaseMongoUUIDEntityAbstract } from 'src/common/database/abstracts/mongo/entities/database.mongo.uuid.entity.abstract';
|
||||||
|
import { DatabaseEntity } from 'src/common/database/decorators/database.decorator';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
import { UserEntity } from "src/modules/user/repository/entities/user.entity";
|
||||||
|
|
||||||
|
export const ApiKeyDatabaseName = 'apikeys';
|
||||||
|
|
||||||
|
@DatabaseEntity({ collection: ApiKeyDatabaseName })
|
||||||
|
export class ApiKeyEntity extends DatabaseMongoUUIDEntityAbstract {
|
||||||
|
@Prop({
|
||||||
|
required: true,
|
||||||
|
ref: UserEntity.name,
|
||||||
|
index: true,
|
||||||
|
})
|
||||||
|
user: string;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
type: String,
|
||||||
|
minlength: 1,
|
||||||
|
maxlength: 100,
|
||||||
|
lowercase: true,
|
||||||
|
trim: true,
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
minlength: 1,
|
||||||
|
maxlength: 255,
|
||||||
|
})
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
trim: true,
|
||||||
|
})
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: true,
|
||||||
|
trim: true,
|
||||||
|
type: String,
|
||||||
|
})
|
||||||
|
hash: string;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
startDate?: Date;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
endDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiKeySchema = SchemaFactory.createForClass(ApiKeyEntity);
|
||||||
|
|
||||||
|
export type ApiKeyDoc = ApiKeyEntity & Document<string>;
|
||||||
|
|
||||||
|
ApiKeySchema.pre(
|
||||||
|
'save',
|
||||||
|
function (next: CallbackWithoutResultAndOptionalError) {
|
||||||
|
this.name = this.name.toLowerCase();
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
);
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ApiKeyEntity,
|
||||||
|
ApiKeyDoc,
|
||||||
|
} from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import { DatabaseMongoUUIDRepositoryAbstract } from 'src/common/database/abstracts/mongo/repositories/database.mongo.uuid.repository.abstract';
|
||||||
|
import { DatabaseModel } from 'src/common/database/decorators/database.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyRepository extends DatabaseMongoUUIDRepositoryAbstract<
|
||||||
|
ApiKeyEntity,
|
||||||
|
ApiKeyDoc
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
@DatabaseModel(ApiKeyEntity.name)
|
||||||
|
private readonly ApiKeyDoc: Model<ApiKeyEntity>
|
||||||
|
) {
|
||||||
|
super(ApiKeyDoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { ApiProperty, PickType } from '@nestjs/swagger';
|
||||||
|
import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization';
|
||||||
|
|
||||||
|
export class ApiKeyCreateSerialization extends PickType(
|
||||||
|
ApiKeyGetSerialization,
|
||||||
|
['key', '_id'] as const
|
||||||
|
) {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Secret key of ApiKey, only show at once',
|
||||||
|
example: true,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
secret: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
import { ResponseIdSerialization } from 'src/common/response/serializations/response.id.serialization';
|
||||||
|
|
||||||
|
export class ApiKeyGetSerialization extends ResponseIdSerialization {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Alias name of api key',
|
||||||
|
example: faker.name.jobTitle(),
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Description of api key',
|
||||||
|
example: 'blabla description',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Unique key of api key',
|
||||||
|
example: faker.random.alpha(115),
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@Exclude()
|
||||||
|
hash: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Active flag of api key',
|
||||||
|
example: true,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Api Key start date',
|
||||||
|
example: faker.date.recent(),
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
startDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Api Key end date',
|
||||||
|
example: faker.date.recent(),
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
endDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Date created at',
|
||||||
|
example: faker.date.recent(),
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
readonly createdAt: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Date updated at',
|
||||||
|
example: faker.date.recent(),
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
readonly updatedAt: Date;
|
||||||
|
|
||||||
|
@Exclude()
|
||||||
|
readonly deletedAt?: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { OmitType } from '@nestjs/swagger';
|
||||||
|
import { ApiKeyGetSerialization } from 'src/common/api-key/serializations/api-key.get.serialization';
|
||||||
|
|
||||||
|
export class ApiKeyListSerialization extends OmitType(ApiKeyGetSerialization, [
|
||||||
|
'description',
|
||||||
|
] as const) {}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { ApiKeyCreateSerialization } from 'src/common/api-key/serializations/api-key.create.serialization';
|
||||||
|
|
||||||
|
export class ApiKeyResetSerialization extends ApiKeyCreateSerialization {}
|
||||||
260
src/common/api-key/services/api-key.service.ts
Normal file
260
src/common/api-key/services/api-key.service.ts
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
IDatabaseCreateOptions,
|
||||||
|
IDatabaseFindAllOptions,
|
||||||
|
IDatabaseFindOneOptions,
|
||||||
|
IDatabaseOptions,
|
||||||
|
IDatabaseManyOptions,
|
||||||
|
IDatabaseExistOptions,
|
||||||
|
} from 'src/common/database/interfaces/database.interface';
|
||||||
|
import { IApiKeyService } from 'src/common/api-key/interfaces/api-key.service.interface';
|
||||||
|
import { IApiKeyCreated } from 'src/common/api-key/interfaces/api-key.interface';
|
||||||
|
import {
|
||||||
|
ApiKeyDoc,
|
||||||
|
ApiKeyEntity,
|
||||||
|
} from 'src/common/api-key/repository/entities/api-key.entity';
|
||||||
|
import { ApiKeyRepository } from 'src/common/api-key/repository/repositories/api-key.repository';
|
||||||
|
import { ApiKeyActiveDto } from 'src/common/api-key/dtos/api-key.active.dto';
|
||||||
|
import { HelperStringService } from 'src/common/helper/services/helper.string.service';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { HelperHashService } from 'src/common/helper/services/helper.hash.service';
|
||||||
|
import {
|
||||||
|
ApiKeyCreateDto,
|
||||||
|
ApiKeyCreateRawDto,
|
||||||
|
} from 'src/common/api-key/dtos/api-key.create.dto';
|
||||||
|
import { ApiKeyUpdateDto } from 'src/common/api-key/dtos/api-key.update.dto';
|
||||||
|
import { ApiKeyUpdateDateDto } from 'src/common/api-key/dtos/api-key.update-date.dto';
|
||||||
|
import { HelperDateService } from 'src/common/helper/services/helper.date.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyService implements IApiKeyService {
|
||||||
|
private readonly env: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly helperStringService: HelperStringService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly helperHashService: HelperHashService,
|
||||||
|
private readonly helperDateService: HelperDateService,
|
||||||
|
private readonly apiKeyRepository: ApiKeyRepository
|
||||||
|
) {
|
||||||
|
this.env = this.configService.get<string>('app.env');
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions
|
||||||
|
): Promise<ApiKeyEntity[]> {
|
||||||
|
return this.apiKeyRepository.findAll<ApiKeyEntity>(find, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneById(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc> {
|
||||||
|
return this.apiKeyRepository.findOneById<ApiKeyDoc>(_id, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc> {
|
||||||
|
return this.apiKeyRepository.findOne<ApiKeyDoc>(find, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async existByUser(
|
||||||
|
user: string,
|
||||||
|
options?: IDatabaseExistOptions
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.apiKeyRepository.exists(
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByKey(
|
||||||
|
key: string,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc> {
|
||||||
|
return this.apiKeyRepository.findOne<ApiKeyDoc>({ key }, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByActiveKey(
|
||||||
|
key: string,
|
||||||
|
options?: IDatabaseFindOneOptions
|
||||||
|
): Promise<ApiKeyDoc> {
|
||||||
|
return this.apiKeyRepository.findOne<ApiKeyDoc>(
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTotal(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseOptions
|
||||||
|
): Promise<number> {
|
||||||
|
return this.apiKeyRepository.getTotal(find, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
user: string,
|
||||||
|
{ name, description, startDate, endDate }: ApiKeyCreateDto,
|
||||||
|
options?: IDatabaseCreateOptions
|
||||||
|
): Promise<IApiKeyCreated> {
|
||||||
|
const key = await this.createKey();
|
||||||
|
const secret = await this.createSecret();
|
||||||
|
const hash: string = await this.createHashApiKey(key, secret);
|
||||||
|
|
||||||
|
const dto: ApiKeyEntity = new ApiKeyEntity();
|
||||||
|
dto.user = user;
|
||||||
|
dto.name = name;
|
||||||
|
dto.description = description;
|
||||||
|
dto.key = key;
|
||||||
|
dto.hash = hash;
|
||||||
|
dto.isActive = true;
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
dto.startDate = startDate;
|
||||||
|
dto.endDate = endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created: ApiKeyDoc =
|
||||||
|
await this.apiKeyRepository.create<ApiKeyEntity>(dto, options);
|
||||||
|
|
||||||
|
return { doc: created, secret };
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRaw(
|
||||||
|
user: string,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
key,
|
||||||
|
secret,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}: ApiKeyCreateRawDto,
|
||||||
|
options?: IDatabaseCreateOptions
|
||||||
|
): Promise<IApiKeyCreated> {
|
||||||
|
const hash: string = await this.createHashApiKey(key, secret);
|
||||||
|
|
||||||
|
const dto: ApiKeyEntity = new ApiKeyEntity();
|
||||||
|
dto.name = name;
|
||||||
|
dto.user = user;
|
||||||
|
dto.description = description;
|
||||||
|
dto.key = key;
|
||||||
|
dto.hash = hash;
|
||||||
|
dto.isActive = true;
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
dto.startDate = this.helperDateService.startOfDay(startDate);
|
||||||
|
dto.endDate = this.helperDateService.endOfDay(endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const created: ApiKeyDoc =
|
||||||
|
await this.apiKeyRepository.create<ApiKeyEntity>(dto, options);
|
||||||
|
|
||||||
|
return { doc: created, secret };
|
||||||
|
}
|
||||||
|
|
||||||
|
async active(repository: ApiKeyDoc): Promise<ApiKeyDoc> {
|
||||||
|
repository.isActive = true;
|
||||||
|
|
||||||
|
return this.apiKeyRepository.save(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async inactive(repository: ApiKeyDoc): Promise<ApiKeyDoc> {
|
||||||
|
repository.isActive = false;
|
||||||
|
|
||||||
|
return this.apiKeyRepository.save(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
repository: ApiKeyDoc,
|
||||||
|
{ name, description }: ApiKeyUpdateDto
|
||||||
|
): Promise<ApiKeyDoc> {
|
||||||
|
repository.name = name;
|
||||||
|
repository.description = description;
|
||||||
|
|
||||||
|
return this.apiKeyRepository.save(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDate(
|
||||||
|
repository: ApiKeyDoc,
|
||||||
|
{ startDate, endDate }: ApiKeyUpdateDateDto
|
||||||
|
): Promise<ApiKeyDoc> {
|
||||||
|
repository.startDate = this.helperDateService.startOfDay(startDate);
|
||||||
|
repository.endDate = this.helperDateService.endOfDay(endDate);
|
||||||
|
|
||||||
|
return this.apiKeyRepository.save(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reset(repository: ApiKeyDoc, secret: string): Promise<ApiKeyDoc> {
|
||||||
|
const hash: string = await this.createHashApiKey(
|
||||||
|
repository.key,
|
||||||
|
secret
|
||||||
|
);
|
||||||
|
|
||||||
|
repository.hash = hash;
|
||||||
|
|
||||||
|
return this.apiKeyRepository.save(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(repository: ApiKeyDoc): Promise<ApiKeyDoc> {
|
||||||
|
return this.apiKeyRepository.softDelete(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateHashApiKey(
|
||||||
|
hashFromRequest: string,
|
||||||
|
hash: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.helperHashService.sha256Compare(hashFromRequest, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createKey(): Promise<string> {
|
||||||
|
return this.helperStringService.random(25, {
|
||||||
|
safe: false,
|
||||||
|
upperCase: true,
|
||||||
|
prefix: `${this.env}_`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSecret(): Promise<string> {
|
||||||
|
return this.helperStringService.random(35, {
|
||||||
|
safe: false,
|
||||||
|
upperCase: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createHashApiKey(key: string, secret: string): Promise<string> {
|
||||||
|
return this.helperHashService.sha256(`${key}:${secret}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseManyOptions
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.apiKeyRepository.deleteMany(find, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async inactiveManyByEndDate(
|
||||||
|
options?: IDatabaseManyOptions
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.apiKeyRepository.updateMany<ApiKeyActiveDto>(
|
||||||
|
{
|
||||||
|
endDate: {
|
||||||
|
$lte: this.helperDateService.create(),
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/common/api-key/tasks/api-key.inactive.task.ts
Normal file
21
src/common/api-key/tasks/api-key.inactive.task.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { ApiKeyService } from 'src/common/api-key/services/api-key.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApiKeyInactiveTask {
|
||||||
|
constructor(private readonly apiKeyService: ApiKeyService) {}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, {
|
||||||
|
name: 'inactiveApiKey',
|
||||||
|
})
|
||||||
|
async inactiveApiKey(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.apiKeyService.inactiveManyByEndDate();
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/common/auth/auth.module.ts
Normal file
12
src/common/auth/auth.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthJwtAccessStrategy } from 'src/common/auth/guards/jwt-access/auth.jwt-access.strategy';
|
||||||
|
import { AuthJwtRefreshStrategy } from 'src/common/auth/guards/jwt-refresh/auth.jwt-refresh.strategy';
|
||||||
|
import { AuthService } from 'src/common/auth/services/auth.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [AuthService, AuthJwtAccessStrategy, AuthJwtRefreshStrategy],
|
||||||
|
exports: [AuthService],
|
||||||
|
controllers: [],
|
||||||
|
imports: [],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
2
src/common/auth/constants/auth.constant.ts
Normal file
2
src/common/auth/constants/auth.constant.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const AUTH_ACCESS_FOR_META_KEY = 'AuthAccessForMetaKey';
|
||||||
|
export const AUTH_PERMISSION_META_KEY = 'AuthPermissionMetaKey';
|
||||||
17
src/common/auth/constants/auth.enum.constant.ts
Normal file
17
src/common/auth/constants/auth.enum.constant.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export enum ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN {
|
||||||
|
SUPER_ADMIN = 'SUPER_ADMIN',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ENUM_AUTH_ACCESS_FOR_DEFAULT {
|
||||||
|
USER = 'USER',
|
||||||
|
ADMIN = 'ADMIN',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ENUM_AUTH_ACCESS_FOR = {
|
||||||
|
...ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN,
|
||||||
|
...ENUM_AUTH_ACCESS_FOR_DEFAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ENUM_AUTH_ACCESS_FOR =
|
||||||
|
| ENUM_AUTH_ACCESS_FOR_SUPER_ADMIN
|
||||||
|
| ENUM_AUTH_ACCESS_FOR_DEFAULT;
|
||||||
32
src/common/auth/constants/auth.enum.permission.constant.ts
Normal file
32
src/common/auth/constants/auth.enum.permission.constant.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export enum ENUM_AUTH_PERMISSIONS {
|
||||||
|
API_KEY_READ = 'API_KEY_READ',
|
||||||
|
API_KEY_UPDATE = 'API_KEY_UPDATE',
|
||||||
|
API_KEY_ACTIVE = 'API_KEY_ACTIVE',
|
||||||
|
API_KEY_INACTIVE = 'API_KEY_INACTIVE',
|
||||||
|
API_KEY_UPDATE_DATE = 'API_KEY_UPDATE_DATE',
|
||||||
|
|
||||||
|
USER_READ = 'USER_READ',
|
||||||
|
USER_CREATE = 'USER_CREATE',
|
||||||
|
USER_UPDATE = 'USER_UPDATE',
|
||||||
|
USER_ACTIVE = 'USER_ACTIVE',
|
||||||
|
USER_INACTIVE = 'USER_INACTIVE',
|
||||||
|
USER_BLOCKED = 'USER_BLOCKED',
|
||||||
|
USER_DELETE = 'USER_DELETE',
|
||||||
|
USER_IMPORT = 'USER_IMPORT',
|
||||||
|
USER_EXPORT = 'USER_EXPORT',
|
||||||
|
|
||||||
|
ROLE_CREATE = 'ROLE_CREATE',
|
||||||
|
ROLE_UPDATE = 'ROLE_UPDATE',
|
||||||
|
ROLE_ACTIVE = 'ROLE_ACTIVE',
|
||||||
|
ROLE_INACTIVE = 'ROLE_INACTIVE',
|
||||||
|
ROLE_READ = 'ROLE_READ',
|
||||||
|
ROLE_DELETE = 'ROLE_DELETE',
|
||||||
|
|
||||||
|
PERMISSION_READ = 'PERMISSION_READ',
|
||||||
|
PERMISSION_UPDATE = 'PERMISSION_UPDATE',
|
||||||
|
PERMISSION_ACTIVE = 'PERMISSION_ACTIVE',
|
||||||
|
PERMISSION_INACTIVE = 'PERMISSION_INACTIVE',
|
||||||
|
|
||||||
|
SETTING_READ = 'SETTING_READ',
|
||||||
|
SETTING_UPDATE = 'SETTING_UPDATE',
|
||||||
|
}
|
||||||
10
src/common/auth/constants/auth.status-code.constant.ts
Normal file
10
src/common/auth/constants/auth.status-code.constant.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export enum ENUM_AUTH_STATUS_CODE_ERROR {
|
||||||
|
AUTH_JWT_ACCESS_TOKEN_ERROR = 5100,
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_ERROR = 5101,
|
||||||
|
AUTH_PERMISSION_TOKEN_ERROR = 5102,
|
||||||
|
AUTH_PERMISSION_TOKEN_INVALID_ERROR = 5103,
|
||||||
|
AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR = 5104,
|
||||||
|
AUTH_ROLE_ACCESS_FOR_INVALID_ERROR = 5105,
|
||||||
|
AUTH_PERMISSION_INVALID_ERROR = 5106,
|
||||||
|
AUTH_ACCESS_FOR_INVALID_ERROR = 5107,
|
||||||
|
}
|
||||||
49
src/common/auth/decorators/auth.jwt.decorator.ts
Normal file
49
src/common/auth/decorators/auth.jwt.decorator.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';
|
||||||
|
import { AUTH_ACCESS_FOR_META_KEY } from 'src/common/auth/constants/auth.constant';
|
||||||
|
import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant';
|
||||||
|
import { AuthJwtAccessGuard } from 'src/common/auth/guards/jwt-access/auth.jwt-access.guard';
|
||||||
|
import { AuthJwtRefreshGuard } from 'src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard';
|
||||||
|
import { AuthPayloadAccessForGuard } from 'src/common/auth/guards/payload/auth.payload.access-for.guard';
|
||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const AuthJwtPayload = createParamDecorator(
|
||||||
|
(data: string, ctx: ExecutionContext): Record<string, any> => {
|
||||||
|
const { user } = ctx.switchToHttp().getRequest();
|
||||||
|
return data ? user[data] : user;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AuthJwtToken = createParamDecorator(
|
||||||
|
(data: string, ctx: ExecutionContext): string => {
|
||||||
|
const { headers } = ctx.switchToHttp().getRequest();
|
||||||
|
const { authorization } = headers;
|
||||||
|
const authorizations: string[] = authorization.split(' ');
|
||||||
|
|
||||||
|
return authorizations.length >= 2 ? authorizations[1] : undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function AuthJwtAccessProtected(): MethodDecorator {
|
||||||
|
return applyDecorators(UseGuards(AuthJwtAccessGuard));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthJwtPublicAccessProtected(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(AuthJwtAccessGuard, AuthPayloadAccessForGuard),
|
||||||
|
SetMetadata(AUTH_ACCESS_FOR_META_KEY, [ENUM_AUTH_ACCESS_FOR.USER])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthJwtAdminAccessProtected(): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(AuthJwtAccessGuard, AuthPayloadAccessForGuard),
|
||||||
|
SetMetadata(AUTH_ACCESS_FOR_META_KEY, [
|
||||||
|
ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN,
|
||||||
|
ENUM_AUTH_ACCESS_FOR.ADMIN,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthJwtRefreshProtected(): MethodDecorator {
|
||||||
|
return applyDecorators(UseGuards(AuthJwtRefreshGuard));
|
||||||
|
}
|
||||||
27
src/common/auth/decorators/auth.permission.decorator.ts
Normal file
27
src/common/auth/decorators/auth.permission.decorator.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
applyDecorators,
|
||||||
|
createParamDecorator,
|
||||||
|
ExecutionContext,
|
||||||
|
SetMetadata,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AUTH_PERMISSION_META_KEY } from 'src/common/auth/constants/auth.constant';
|
||||||
|
import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant';
|
||||||
|
import { AuthPayloadPermissionGuard } from 'src/common/auth/guards/payload/auth.payload.permission.guard';
|
||||||
|
import { AuthPermissionGuard } from 'src/common/auth/guards/permission/auth.permission.guard';
|
||||||
|
|
||||||
|
export const AuthPermissionPayload = createParamDecorator(
|
||||||
|
(data: string, ctx: ExecutionContext): Record<string, any> => {
|
||||||
|
const { permissions } = ctx.switchToHttp().getRequest();
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function AuthPermissionProtected(
|
||||||
|
...permissions: ENUM_AUTH_PERMISSIONS[]
|
||||||
|
): MethodDecorator {
|
||||||
|
return applyDecorators(
|
||||||
|
UseGuards(AuthPermissionGuard, AuthPayloadPermissionGuard),
|
||||||
|
SetMetadata(AUTH_PERMISSION_META_KEY, permissions)
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts
Normal file
19
src/common/auth/guards/jwt-access/auth.jwt-access.guard.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthJwtAccessGuard extends AuthGuard('jwt') {
|
||||||
|
handleRequest<TUser = any>(err: Error, user: TUser, info: Error): TUser {
|
||||||
|
if (err || !user) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR,
|
||||||
|
message: 'auth.error.accessTokenUnauthorized',
|
||||||
|
_error: err ? err.message : info.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthService } from 'src/common/auth/services/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthJwtAccessStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly authService: AuthService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme(
|
||||||
|
configService.get<string>('auth.prefixAuthorization')
|
||||||
|
),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
jsonWebTokenOptions: {
|
||||||
|
ignoreNotBefore: false,
|
||||||
|
audience: configService.get<string>('auth.audience'),
|
||||||
|
issuer: configService.get<string>('auth.issuer'),
|
||||||
|
subject: configService.get<string>('auth.subject'),
|
||||||
|
},
|
||||||
|
secretOrKey: configService.get<string>(
|
||||||
|
'auth.accessToken.secretKey'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate({
|
||||||
|
data,
|
||||||
|
}: Record<string, any>): Promise<Record<string, any>> {
|
||||||
|
const payloadEncryption: boolean =
|
||||||
|
await this.authService.getPayloadEncryption();
|
||||||
|
|
||||||
|
return payloadEncryption
|
||||||
|
? this.authService.decryptAccessToken({ data })
|
||||||
|
: data;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts
Normal file
19
src/common/auth/guards/jwt-refresh/auth.jwt-refresh.guard.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthJwtRefreshGuard extends AuthGuard('jwtRefresh') {
|
||||||
|
handleRequest<TUser = any>(err: Error, user: TUser, info: Error): TUser {
|
||||||
|
if (err || !user) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_REFRESH_TOKEN_ERROR,
|
||||||
|
message: 'auth.error.refreshTokenUnauthorized',
|
||||||
|
_error: err ? err.message : info.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthService } from 'src/common/auth/services/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthJwtRefreshStrategy extends PassportStrategy(
|
||||||
|
Strategy,
|
||||||
|
'jwtRefresh'
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly authService: AuthService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme(
|
||||||
|
configService.get<string>('auth.prefixAuthorization')
|
||||||
|
),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
jsonWebTokenOptions: {
|
||||||
|
ignoreNotBefore: false,
|
||||||
|
audience: configService.get<string>('auth.audience'),
|
||||||
|
issuer: configService.get<string>('auth.issuer'),
|
||||||
|
subject: configService.get<string>('auth.subject'),
|
||||||
|
},
|
||||||
|
secretOrKey: configService.get<string>(
|
||||||
|
'auth.refreshToken.secretKey'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate({
|
||||||
|
data,
|
||||||
|
}: Record<string, any>): Promise<Record<string, any>> {
|
||||||
|
const payloadEncryption: boolean =
|
||||||
|
await this.authService.getPayloadEncryption();
|
||||||
|
|
||||||
|
return payloadEncryption
|
||||||
|
? this.authService.decryptRefreshToken({ data })
|
||||||
|
: data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { AUTH_ACCESS_FOR_META_KEY } from 'src/common/auth/constants/auth.constant';
|
||||||
|
import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant';
|
||||||
|
import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant';
|
||||||
|
import { HelperArrayService } from 'src/common/helper/services/helper.array.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthPayloadAccessForGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly helperArrayService: HelperArrayService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const requiredFor: ENUM_AUTH_ACCESS_FOR[] =
|
||||||
|
this.reflector.getAllAndOverride<ENUM_AUTH_ACCESS_FOR[]>(
|
||||||
|
AUTH_ACCESS_FOR_META_KEY,
|
||||||
|
[context.getHandler(), context.getClass()]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
const { accessFor } = user;
|
||||||
|
|
||||||
|
if (!requiredFor || accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFor: boolean = this.helperArrayService.includes(
|
||||||
|
requiredFor,
|
||||||
|
accessFor
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasFor) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_ACCESS_FOR_INVALID_ERROR,
|
||||||
|
message: 'auth.error.accessForForbidden',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hasFor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { AUTH_PERMISSION_META_KEY } from 'src/common/auth/constants/auth.constant';
|
||||||
|
import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant';
|
||||||
|
import { ENUM_AUTH_PERMISSIONS } from 'src/common/auth/constants/auth.enum.permission.constant';
|
||||||
|
import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant';
|
||||||
|
import { HelperArrayService } from 'src/common/helper/services/helper.array.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthPayloadPermissionGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly helperArrayService: HelperArrayService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const requiredPermission: ENUM_AUTH_PERMISSIONS[] =
|
||||||
|
this.reflector.getAllAndOverride<ENUM_AUTH_PERMISSIONS[]>(
|
||||||
|
AUTH_PERMISSION_META_KEY,
|
||||||
|
[context.getHandler(), context.getClass()]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { permissions, user } = context.switchToHttp().getRequest();
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR,
|
||||||
|
message: 'auth.error.accessTokenUnauthorized',
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
!requiredPermission ||
|
||||||
|
user.accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPermission: boolean = this.helperArrayService.in(
|
||||||
|
permissions,
|
||||||
|
requiredPermission
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_INVALID_ERROR,
|
||||||
|
message: 'auth.error.permissionForbidden',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hasPermission;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/common/auth/guards/permission/auth.permission.guard.ts
Normal file
81
src/common/auth/guards/permission/auth.permission.guard.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
UnauthorizedException,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { ENUM_AUTH_ACCESS_FOR } from 'src/common/auth/constants/auth.enum.constant';
|
||||||
|
import { ENUM_AUTH_STATUS_CODE_ERROR } from 'src/common/auth/constants/auth.status-code.constant';
|
||||||
|
import { AuthService } from 'src/common/auth/services/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthPermissionGuard implements CanActivate {
|
||||||
|
private readonly headerName: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly authService: AuthService
|
||||||
|
) {
|
||||||
|
this.headerName = this.configService.get<string>(
|
||||||
|
'auth.permissionToken.headerName'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const { user } = request;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_JWT_ACCESS_TOKEN_ERROR,
|
||||||
|
message: 'auth.error.accessTokenUnauthorized',
|
||||||
|
});
|
||||||
|
} else if (user.accessFor === ENUM_AUTH_ACCESS_FOR.SUPER_ADMIN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { [this.headerName]: token } = request.headers;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_ERROR,
|
||||||
|
message: 'auth.error.permissionTokenUnauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate: boolean =
|
||||||
|
await this.authService.validatePermissionToken(token);
|
||||||
|
if (!validate) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_INVALID_ERROR,
|
||||||
|
message: 'auth.error.permissionTokenInvalid',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await this.authService.payloadPermissionToken(token);
|
||||||
|
|
||||||
|
const payloadEncryption = await this.authService.getPayloadEncryption();
|
||||||
|
let payloadDecryptPermissionToken: Record<string, any> = data;
|
||||||
|
|
||||||
|
if (payloadEncryption) {
|
||||||
|
payloadDecryptPermissionToken =
|
||||||
|
await this.authService.decryptPermissionToken(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadDecryptPermissionToken._id !== user._id) {
|
||||||
|
throw new ForbiddenException({
|
||||||
|
statusCode:
|
||||||
|
ENUM_AUTH_STATUS_CODE_ERROR.AUTH_PERMISSION_TOKEN_NOT_YOUR_ERROR,
|
||||||
|
message: 'auth.error.permissionTokenNotYour',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
request.permissions = payloadDecryptPermissionToken.permissions;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/common/auth/interfaces/auth.interface.ts
Normal file
17
src/common/auth/interfaces/auth.interface.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Auth
|
||||||
|
export interface IAuthPassword {
|
||||||
|
salt: string;
|
||||||
|
passwordHash: string;
|
||||||
|
passwordExpired: Date;
|
||||||
|
passwordCreated: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthPayloadOptions {
|
||||||
|
loginDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthRefreshTokenOptions {
|
||||||
|
// in milis
|
||||||
|
notBeforeExpirationTime?: number | string;
|
||||||
|
rememberMe?: boolean;
|
||||||
|
}
|
||||||
93
src/common/auth/interfaces/auth.service.interface.ts
Normal file
93
src/common/auth/interfaces/auth.service.interface.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
IAuthPassword,
|
||||||
|
IAuthPayloadOptions,
|
||||||
|
IAuthRefreshTokenOptions,
|
||||||
|
} from 'src/common/auth/interfaces/auth.interface';
|
||||||
|
|
||||||
|
export interface IAuthService {
|
||||||
|
encryptAccessToken(payload: Record<string, any>): Promise<string>;
|
||||||
|
|
||||||
|
decryptAccessToken(
|
||||||
|
payload: Record<string, any>
|
||||||
|
): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
createAccessToken(
|
||||||
|
payloadHashed: string | Record<string, any>
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
|
validateAccessToken(token: string): Promise<boolean>;
|
||||||
|
|
||||||
|
payloadAccessToken(token: string): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
encryptRefreshToken(payload: Record<string, any>): Promise<string>;
|
||||||
|
|
||||||
|
decryptRefreshToken(
|
||||||
|
payload: Record<string, any>
|
||||||
|
): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
createRefreshToken(
|
||||||
|
payloadHashed: string | Record<string, any>,
|
||||||
|
options?: IAuthRefreshTokenOptions
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
|
validateRefreshToken(token: string): Promise<boolean>;
|
||||||
|
|
||||||
|
payloadRefreshToken(token: string): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
encryptPermissionToken(payload: Record<string, any>): Promise<string>;
|
||||||
|
|
||||||
|
decryptPermissionToken({
|
||||||
|
data,
|
||||||
|
}: Record<string, any>): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
createPermissionToken(
|
||||||
|
payloadHashed: string | Record<string, any>
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
|
validatePermissionToken(token: string): Promise<boolean>;
|
||||||
|
|
||||||
|
payloadPermissionToken(token: string): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
validateUser(
|
||||||
|
passwordString: string,
|
||||||
|
passwordHash: string
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
createPayloadAccessToken(
|
||||||
|
data: Record<string, any>,
|
||||||
|
rememberMe: boolean,
|
||||||
|
options?: IAuthPayloadOptions
|
||||||
|
): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
createPayloadRefreshToken(
|
||||||
|
_id: string,
|
||||||
|
rememberMe: boolean,
|
||||||
|
options?: IAuthPayloadOptions
|
||||||
|
): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
createPayloadPermissionToken(
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
createSalt(length: number): Promise<string>;
|
||||||
|
|
||||||
|
createPassword(password: string): Promise<IAuthPassword>;
|
||||||
|
|
||||||
|
checkPasswordExpired(passwordExpired: Date): Promise<boolean>;
|
||||||
|
|
||||||
|
getTokenType(): Promise<string>;
|
||||||
|
|
||||||
|
getAccessTokenExpirationTime(): Promise<number>;
|
||||||
|
|
||||||
|
getRefreshTokenExpirationTime(rememberMe?: boolean): Promise<number>;
|
||||||
|
|
||||||
|
getIssuer(): Promise<string>;
|
||||||
|
|
||||||
|
getAudience(): Promise<string>;
|
||||||
|
|
||||||
|
getSubject(): Promise<string>;
|
||||||
|
|
||||||
|
getPayloadEncryption(): Promise<boolean>;
|
||||||
|
|
||||||
|
getPermissionTokenExpirationTime(): Promise<number>;
|
||||||
|
}
|
||||||
371
src/common/auth/services/auth.service.ts
Normal file
371
src/common/auth/services/auth.service.ts
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import {
|
||||||
|
IAuthPassword,
|
||||||
|
IAuthPayloadOptions,
|
||||||
|
IAuthRefreshTokenOptions,
|
||||||
|
} from 'src/common/auth/interfaces/auth.interface';
|
||||||
|
import { IAuthService } from 'src/common/auth/interfaces/auth.service.interface';
|
||||||
|
import { HelperDateService } from 'src/common/helper/services/helper.date.service';
|
||||||
|
import { HelperEncryptionService } from 'src/common/helper/services/helper.encryption.service';
|
||||||
|
import { HelperHashService } from 'src/common/helper/services/helper.hash.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService implements IAuthService {
|
||||||
|
private readonly accessTokenSecretKey: string;
|
||||||
|
private readonly accessTokenExpirationTime: number;
|
||||||
|
private readonly accessTokenNotBeforeExpirationTime: number;
|
||||||
|
private readonly accessTokenEncryptKey: string;
|
||||||
|
private readonly accessTokenEncryptIv: string;
|
||||||
|
|
||||||
|
private readonly refreshTokenSecretKey: string;
|
||||||
|
private readonly refreshTokenExpirationTime: number;
|
||||||
|
private readonly refreshTokenExpirationTimeRememberMe: number;
|
||||||
|
private readonly refreshTokenNotBeforeExpirationTime: number;
|
||||||
|
private readonly refreshTokenEncryptKey: string;
|
||||||
|
private readonly refreshTokenEncryptIv: string;
|
||||||
|
|
||||||
|
private readonly payloadEncryption: boolean;
|
||||||
|
private readonly prefixAuthorization: string;
|
||||||
|
private readonly audience: string;
|
||||||
|
private readonly issuer: string;
|
||||||
|
private readonly subject: string;
|
||||||
|
|
||||||
|
private readonly passwordExpiredIn: number;
|
||||||
|
private readonly passwordSaltLength: number;
|
||||||
|
|
||||||
|
private readonly permissionTokenSecretToken: string;
|
||||||
|
private readonly permissionTokenExpirationTime: number;
|
||||||
|
private readonly permissionTokenNotBeforeExpirationTime: number;
|
||||||
|
private readonly permissionTokenEncryptKey: string;
|
||||||
|
private readonly permissionTokenEncryptIv: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly helperHashService: HelperHashService,
|
||||||
|
private readonly helperDateService: HelperDateService,
|
||||||
|
private readonly helperEncryptionService: HelperEncryptionService,
|
||||||
|
private readonly configService: ConfigService
|
||||||
|
) {
|
||||||
|
this.accessTokenSecretKey = this.configService.get<string>(
|
||||||
|
'auth.accessToken.secretKey'
|
||||||
|
);
|
||||||
|
this.accessTokenExpirationTime = this.configService.get<number>(
|
||||||
|
'auth.accessToken.expirationTime'
|
||||||
|
);
|
||||||
|
this.accessTokenNotBeforeExpirationTime =
|
||||||
|
this.configService.get<number>(
|
||||||
|
'auth.accessToken.notBeforeExpirationTime'
|
||||||
|
);
|
||||||
|
this.accessTokenEncryptKey = this.configService.get<string>(
|
||||||
|
'auth.accessToken.encryptKey'
|
||||||
|
);
|
||||||
|
this.accessTokenEncryptIv = this.configService.get<string>(
|
||||||
|
'auth.accessToken.encryptIv'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.refreshTokenSecretKey = this.configService.get<string>(
|
||||||
|
'auth.refreshToken.secretKey'
|
||||||
|
);
|
||||||
|
this.refreshTokenExpirationTime = this.configService.get<number>(
|
||||||
|
'auth.refreshToken.expirationTime'
|
||||||
|
);
|
||||||
|
this.refreshTokenExpirationTimeRememberMe =
|
||||||
|
this.configService.get<number>(
|
||||||
|
'auth.refreshToken.expirationTimeRememberMe'
|
||||||
|
);
|
||||||
|
this.refreshTokenNotBeforeExpirationTime =
|
||||||
|
this.configService.get<number>(
|
||||||
|
'auth.refreshToken.notBeforeExpirationTime'
|
||||||
|
);
|
||||||
|
this.refreshTokenEncryptKey = this.configService.get<string>(
|
||||||
|
'auth.refreshToken.encryptKey'
|
||||||
|
);
|
||||||
|
this.refreshTokenEncryptIv = this.configService.get<string>(
|
||||||
|
'auth.refreshToken.encryptIv'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.payloadEncryption = this.configService.get<boolean>(
|
||||||
|
'auth.payloadEncryption'
|
||||||
|
);
|
||||||
|
this.prefixAuthorization = this.configService.get<string>(
|
||||||
|
'auth.prefixAuthorization'
|
||||||
|
);
|
||||||
|
this.subject = this.configService.get<string>('auth.subject');
|
||||||
|
this.audience = this.configService.get<string>('auth.audience');
|
||||||
|
this.issuer = this.configService.get<string>('auth.issuer');
|
||||||
|
|
||||||
|
this.passwordExpiredIn = this.configService.get<number>(
|
||||||
|
'auth.password.expiredIn'
|
||||||
|
);
|
||||||
|
this.passwordSaltLength = this.configService.get<number>(
|
||||||
|
'auth.password.saltLength'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.permissionTokenSecretToken = this.configService.get<string>(
|
||||||
|
'auth.permissionToken.secretKey'
|
||||||
|
);
|
||||||
|
this.permissionTokenExpirationTime = this.configService.get<number>(
|
||||||
|
'auth.permissionToken.expirationTime'
|
||||||
|
);
|
||||||
|
this.permissionTokenNotBeforeExpirationTime =
|
||||||
|
this.configService.get<number>(
|
||||||
|
'auth.permissionToken.notBeforeExpirationTime'
|
||||||
|
);
|
||||||
|
this.permissionTokenEncryptKey = this.configService.get<string>(
|
||||||
|
'auth.permissionToken.encryptKey'
|
||||||
|
);
|
||||||
|
this.permissionTokenEncryptIv = this.configService.get<string>(
|
||||||
|
'auth.permissionToken.encryptIv'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSalt(length: number): Promise<string> {
|
||||||
|
return this.helperHashService.randomSalt(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptAccessToken(payload: Record<string, any>): Promise<string> {
|
||||||
|
return this.helperEncryptionService.aes256Encrypt(
|
||||||
|
payload,
|
||||||
|
this.accessTokenEncryptKey,
|
||||||
|
this.accessTokenEncryptIv
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptAccessToken({
|
||||||
|
data,
|
||||||
|
}: Record<string, any>): Promise<Record<string, any>> {
|
||||||
|
return this.helperEncryptionService.aes256Decrypt(
|
||||||
|
data,
|
||||||
|
this.accessTokenEncryptKey,
|
||||||
|
this.accessTokenEncryptIv
|
||||||
|
) as Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAccessToken(
|
||||||
|
payloadHashed: string | Record<string, any>
|
||||||
|
): Promise<string> {
|
||||||
|
return this.helperEncryptionService.jwtEncrypt(
|
||||||
|
{ data: payloadHashed },
|
||||||
|
{
|
||||||
|
secretKey: this.accessTokenSecretKey,
|
||||||
|
expiredIn: this.accessTokenExpirationTime,
|
||||||
|
notBefore: this.accessTokenNotBeforeExpirationTime,
|
||||||
|
audience: this.audience,
|
||||||
|
issuer: this.issuer,
|
||||||
|
subject: this.subject,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAccessToken(token: string): Promise<boolean> {
|
||||||
|
return this.helperEncryptionService.jwtVerify(token, {
|
||||||
|
secretKey: this.accessTokenSecretKey,
|
||||||
|
audience: this.audience,
|
||||||
|
issuer: this.issuer,
|
||||||
|
subject: this.subject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async payloadAccessToken(token: string): Promise<Record<string, any>> {
|
||||||
|
return this.helperEncryptionService.jwtDecrypt(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptRefreshToken(payload: Record<string, any>): Promise<string> {
|
||||||
|
return this.helperEncryptionService.aes256Encrypt(
|
||||||
|
payload,
|
||||||
|
this.refreshTokenEncryptKey,
|
||||||
|
this.refreshTokenEncryptIv
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptRefreshToken({
|
||||||
|
data,
|
||||||
|
}: Record<string, any>): Promise<Record<string, any>> {
|
||||||
|
return this.helperEncryptionService.aes256Decrypt(
|
||||||
|
data,
|
||||||
|
this.refreshTokenEncryptKey,
|
||||||
|
this.refreshTokenEncryptIv
|
||||||
|
) as Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRefreshToken(
|
||||||
|
payloadHashed: string | Record<string, any>,
|
||||||
|
options?: IAuthRefreshTokenOptions
|
||||||
|
): Promise<string> {
|
||||||
|
return this.helperEncryptionService.jwtEncrypt(
|
||||||
|
{ data: payloadHashed },
|
||||||
|
{
|
||||||
|
secretKey: this.refreshTokenSecretKey,
|
||||||
|
expiredIn: options?.rememberMe
|
||||||
|
? this.refreshTokenExpirationTimeRememberMe
|
||||||
|
: this.refreshTokenExpirationTime,
|
||||||
|
notBefore:
|
||||||
|
options?.notBeforeExpirationTime ??
|
||||||
|
this.refreshTokenNotBeforeExpirationTime,
|
||||||
|
audience: this.audience,
|
||||||
|
issuer: this.issuer,
|
||||||
|
subject: this.subject,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateRefreshToken(token: string): Promise<boolean> {
|
||||||
|
return this.helperEncryptionService.jwtVerify(token, {
|
||||||
|
secretKey: this.refreshTokenSecretKey,
|
||||||
|
audience: this.audience,
|
||||||
|
issuer: this.issuer,
|
||||||
|
subject: this.subject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async payloadRefreshToken(token: string): Promise<Record<string, any>> {
|
||||||
|
return this.helperEncryptionService.jwtDecrypt(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptPermissionToken(
|
||||||
|
payload: Record<string, any>
|
||||||
|
): Promise<string> {
|
||||||
|
return this.helperEncryptionService.aes256Encrypt(
|
||||||
|
payload,
|
||||||
|
this.permissionTokenEncryptKey,
|
||||||
|
this.permissionTokenEncryptIv
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptPermissionToken({
|
||||||
|
data,
|
||||||
|
}: Record<string, any>): Promise<Record<string, any>> {
|
||||||
|
return this.helperEncryptionService.aes256Decrypt(
|
||||||
|
data,
|
||||||
|
this.permissionTokenEncryptKey,
|
||||||
|
this.permissionTokenEncryptIv
|
||||||
|
) as Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPermissionToken(
|
||||||
|
payloadHashed: string | Record<string, any>
|
||||||
|
): Promise<string> {
|
||||||
|
return this.helperEncryptionService.jwtEncrypt(
|
||||||
|
{ data: payloadHashed },
|
||||||
|
{
|
||||||
|
secretKey: this.permissionTokenSecretToken,
|
||||||
|
expiredIn: this.permissionTokenExpirationTime,
|
||||||
|
notBefore: this.permissionTokenNotBeforeExpirationTime,
|
||||||
|
audience: this.audience,
|
||||||
|
issuer: this.issuer,
|
||||||
|
subject: this.subject,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validatePermissionToken(token: string): Promise<boolean> {
|
||||||
|
return this.helperEncryptionService.jwtVerify(token, {
|
||||||
|
secretKey: this.permissionTokenSecretToken,
|
||||||
|
audience: this.audience,
|
||||||
|
issuer: this.issuer,
|
||||||
|
subject: this.subject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async payloadPermissionToken(token: string): Promise<Record<string, any>> {
|
||||||
|
return this.helperEncryptionService.jwtDecrypt(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateUser(
|
||||||
|
passwordString: string,
|
||||||
|
passwordHash: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.helperHashService.bcryptCompare(
|
||||||
|
passwordString,
|
||||||
|
passwordHash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPayloadAccessToken(
|
||||||
|
data: Record<string, any>,
|
||||||
|
rememberMe: boolean,
|
||||||
|
options?: IAuthPayloadOptions
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
rememberMe,
|
||||||
|
loginDate: options?.loginDate ?? this.helperDateService.create(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPayloadRefreshToken(
|
||||||
|
_id: string,
|
||||||
|
rememberMe: boolean,
|
||||||
|
options?: IAuthPayloadOptions
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
return {
|
||||||
|
_id,
|
||||||
|
rememberMe,
|
||||||
|
loginDate: options?.loginDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPayloadPermissionToken(
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPassword(password: string): Promise<IAuthPassword> {
|
||||||
|
const salt: string = this.helperHashService.randomSalt(20);
|
||||||
|
|
||||||
|
const passwordExpired: Date = this.helperDateService.forwardInSeconds(
|
||||||
|
this.passwordExpiredIn
|
||||||
|
);
|
||||||
|
const passwordCreated: Date = this.helperDateService.create();
|
||||||
|
const passwordHash = this.helperHashService.bcrypt(password, salt);
|
||||||
|
return {
|
||||||
|
passwordHash,
|
||||||
|
passwordExpired,
|
||||||
|
passwordCreated,
|
||||||
|
salt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkPasswordExpired(passwordExpired: Date): Promise<boolean> {
|
||||||
|
const today: Date = this.helperDateService.create();
|
||||||
|
const passwordExpiredConvert: Date =
|
||||||
|
this.helperDateService.create(passwordExpired);
|
||||||
|
|
||||||
|
return today > passwordExpiredConvert;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTokenType(): Promise<string> {
|
||||||
|
return this.prefixAuthorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessTokenExpirationTime(): Promise<number> {
|
||||||
|
return this.accessTokenExpirationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRefreshTokenExpirationTime(rememberMe?: boolean): Promise<number> {
|
||||||
|
return rememberMe
|
||||||
|
? this.refreshTokenExpirationTimeRememberMe
|
||||||
|
: this.refreshTokenExpirationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIssuer(): Promise<string> {
|
||||||
|
return this.issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAudience(): Promise<string> {
|
||||||
|
return this.audience;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSubject(): Promise<string> {
|
||||||
|
return this.subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPayloadEncryption(): Promise<boolean> {
|
||||||
|
return this.payloadEncryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPermissionTokenExpirationTime(): Promise<number> {
|
||||||
|
return this.permissionTokenExpirationTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/common/aws/aws.module.ts
Normal file
10
src/common/aws/aws.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AwsS3Service } from './services/aws.s3.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [AwsS3Service],
|
||||||
|
providers: [AwsS3Service],
|
||||||
|
imports: [],
|
||||||
|
controllers: [],
|
||||||
|
})
|
||||||
|
export class AwsModule {}
|
||||||
1
src/common/aws/constants/aws.s3.constant.ts
Normal file
1
src/common/aws/constants/aws.s3.constant.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const AwsS3MaxPartNumber = 10000;
|
||||||
6
src/common/aws/interfaces/aws.interface.ts
Normal file
6
src/common/aws/interfaces/aws.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ObjectCannedACL } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
export interface IAwsS3PutItemOptions {
|
||||||
|
path: string;
|
||||||
|
acl?: ObjectCannedACL;
|
||||||
|
}
|
||||||
63
src/common/aws/interfaces/aws.s3-service.interface.ts
Normal file
63
src/common/aws/interfaces/aws.s3-service.interface.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
CompletedPart,
|
||||||
|
HeadBucketCommandOutput,
|
||||||
|
UploadPartRequest,
|
||||||
|
} from '@aws-sdk/client-s3';
|
||||||
|
import { IAwsS3PutItemOptions } from 'src/common/aws/interfaces/aws.interface';
|
||||||
|
import {
|
||||||
|
AwsS3MultipartPartsSerialization,
|
||||||
|
AwsS3MultipartSerialization,
|
||||||
|
} from 'src/common/aws/serializations/aws.s3-multipart.serialization';
|
||||||
|
import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
export interface IAwsS3Service {
|
||||||
|
checkConnection(): Promise<HeadBucketCommandOutput>;
|
||||||
|
|
||||||
|
listBucket(): Promise<string[]>;
|
||||||
|
|
||||||
|
listItemInBucket(prefix?: string): Promise<AwsS3Serialization[]>;
|
||||||
|
|
||||||
|
getItemInBucket(
|
||||||
|
filename: string,
|
||||||
|
path?: string
|
||||||
|
): Promise<Readable | ReadableStream<any> | Blob>;
|
||||||
|
|
||||||
|
putItemInBucket(
|
||||||
|
filename: string,
|
||||||
|
content:
|
||||||
|
| string
|
||||||
|
| Uint8Array
|
||||||
|
| Buffer
|
||||||
|
| Readable
|
||||||
|
| ReadableStream
|
||||||
|
| Blob,
|
||||||
|
options?: IAwsS3PutItemOptions
|
||||||
|
): Promise<AwsS3Serialization>;
|
||||||
|
|
||||||
|
deleteItemInBucket(filename: string): Promise<void>;
|
||||||
|
|
||||||
|
deleteItemsInBucket(filenames: string[]): Promise<void>;
|
||||||
|
|
||||||
|
deleteFolder(dir: string): Promise<void>;
|
||||||
|
|
||||||
|
createMultiPart(
|
||||||
|
filename: string,
|
||||||
|
options?: IAwsS3PutItemOptions
|
||||||
|
): Promise<AwsS3MultipartSerialization>;
|
||||||
|
|
||||||
|
uploadPart(
|
||||||
|
path: string,
|
||||||
|
content: UploadPartRequest['Body'] | string | Uint8Array | Buffer,
|
||||||
|
uploadId: string,
|
||||||
|
partNumber: number
|
||||||
|
): Promise<AwsS3MultipartPartsSerialization>;
|
||||||
|
|
||||||
|
completeMultipart(
|
||||||
|
path: string,
|
||||||
|
uploadId: string,
|
||||||
|
parts: CompletedPart[]
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
abortMultipart(path: string, uploadId: string): Promise<void>;
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization';
|
||||||
|
|
||||||
|
export class AwsS3MultipartPartsSerialization {
|
||||||
|
@ApiProperty({
|
||||||
|
example: faker.random.alpha(10),
|
||||||
|
description: 'ETag from aws after init multipart',
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
ETag: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 1,
|
||||||
|
})
|
||||||
|
@Type(() => Number)
|
||||||
|
PartNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AwsS3MultipartSerialization extends AwsS3Serialization {
|
||||||
|
@ApiProperty({
|
||||||
|
example: faker.random.alpha(20),
|
||||||
|
description: 'Upload id from aws after init multipart',
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
uploadId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 1,
|
||||||
|
description: 'Last part number uploaded',
|
||||||
|
})
|
||||||
|
@Type(() => Number)
|
||||||
|
partNumber?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: 200,
|
||||||
|
description: 'Max part number, or length of the chunk',
|
||||||
|
})
|
||||||
|
@Type(() => Number)
|
||||||
|
maxPartNumber?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
$ref: getSchemaPath(AwsS3MultipartPartsSerialization),
|
||||||
|
type: 'array',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
@Type(() => AwsS3MultipartPartsSerialization)
|
||||||
|
parts?: AwsS3MultipartPartsSerialization[];
|
||||||
|
}
|
||||||
41
src/common/aws/serializations/aws.s3.serialization.ts
Normal file
41
src/common/aws/serializations/aws.s3.serialization.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class AwsS3Serialization {
|
||||||
|
@ApiProperty({
|
||||||
|
example: faker.system.directoryPath(),
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: faker.system.filePath(),
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
pathWithFilename: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: faker.system.fileName(),
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
filename: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: `${faker.internet.url()}/${faker.system.filePath()}`,
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
completedUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: faker.internet.url(),
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: faker.system.mimeType(),
|
||||||
|
})
|
||||||
|
@Type(() => String)
|
||||||
|
mime: string;
|
||||||
|
}
|
||||||
377
src/common/aws/services/aws.s3.service.ts
Normal file
377
src/common/aws/services/aws.s3.service.ts
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { IAwsS3PutItemOptions } from 'src/common/aws/interfaces/aws.interface';
|
||||||
|
import { IAwsS3Service } from 'src/common/aws/interfaces/aws.s3-service.interface';
|
||||||
|
import {
|
||||||
|
AwsS3MultipartPartsSerialization,
|
||||||
|
AwsS3MultipartSerialization,
|
||||||
|
} from 'src/common/aws/serializations/aws.s3-multipart.serialization';
|
||||||
|
import { AwsS3Serialization } from 'src/common/aws/serializations/aws.s3.serialization';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import {
|
||||||
|
S3Client,
|
||||||
|
GetObjectCommand,
|
||||||
|
ListBucketsCommand,
|
||||||
|
ListObjectsV2Command,
|
||||||
|
PutObjectCommand,
|
||||||
|
DeleteObjectCommand,
|
||||||
|
DeleteObjectsCommand,
|
||||||
|
ObjectIdentifier,
|
||||||
|
CreateMultipartUploadCommand,
|
||||||
|
CreateMultipartUploadCommandInput,
|
||||||
|
UploadPartCommandInput,
|
||||||
|
UploadPartCommand,
|
||||||
|
CompleteMultipartUploadCommandInput,
|
||||||
|
CompleteMultipartUploadCommand,
|
||||||
|
CompletedPart,
|
||||||
|
GetObjectCommandInput,
|
||||||
|
AbortMultipartUploadCommand,
|
||||||
|
AbortMultipartUploadCommandInput,
|
||||||
|
UploadPartRequest,
|
||||||
|
HeadBucketCommand,
|
||||||
|
HeadBucketCommandOutput,
|
||||||
|
ListBucketsOutput,
|
||||||
|
Bucket,
|
||||||
|
ListObjectsV2Output,
|
||||||
|
_Object,
|
||||||
|
GetObjectOutput,
|
||||||
|
} from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AwsS3Service implements IAwsS3Service {
|
||||||
|
private readonly s3Client: S3Client;
|
||||||
|
private readonly bucket: string;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
this.s3Client = new S3Client({
|
||||||
|
credentials: {
|
||||||
|
accessKeyId:
|
||||||
|
this.configService.get<string>('aws.credential.key'),
|
||||||
|
secretAccessKey: this.configService.get<string>(
|
||||||
|
'aws.credential.secret'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
region: this.configService.get<string>('aws.s3.region'),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bucket = this.configService.get<string>('aws.s3.bucket');
|
||||||
|
this.baseUrl = this.configService.get<string>('aws.s3.baseUrl');
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkConnection(): Promise<HeadBucketCommandOutput> {
|
||||||
|
const command: HeadBucketCommand = new HeadBucketCommand({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const check: HeadBucketCommandOutput = await this.s3Client.send(
|
||||||
|
command
|
||||||
|
);
|
||||||
|
|
||||||
|
return check;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listBucket(): Promise<string[]> {
|
||||||
|
const command: ListBucketsCommand = new ListBucketsCommand({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listBucket: ListBucketsOutput = await this.s3Client.send(
|
||||||
|
command
|
||||||
|
);
|
||||||
|
const mapList = listBucket.Buckets.map((val: Bucket) => val.Name);
|
||||||
|
|
||||||
|
return mapList;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listItemInBucket(prefix?: string): Promise<AwsS3Serialization[]> {
|
||||||
|
const command: ListObjectsV2Command = new ListObjectsV2Command({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Prefix: prefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listItems: ListObjectsV2Output = await this.s3Client.send(
|
||||||
|
command
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapList = listItems.Contents.map((val: _Object) => {
|
||||||
|
const lastIndex: number = val.Key.lastIndexOf('/');
|
||||||
|
const path: string = val.Key.substring(0, lastIndex);
|
||||||
|
const filename: string = val.Key.substring(
|
||||||
|
lastIndex,
|
||||||
|
val.Key.length
|
||||||
|
);
|
||||||
|
const mime: string = filename
|
||||||
|
.substring(filename.lastIndexOf('.') + 1, filename.length)
|
||||||
|
.toLocaleUpperCase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
pathWithFilename: val.Key,
|
||||||
|
filename: filename,
|
||||||
|
completedUrl: `${this.baseUrl}/${val.Key}`,
|
||||||
|
baseUrl: this.baseUrl,
|
||||||
|
mime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapList;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getItemInBucket(
|
||||||
|
filename: string,
|
||||||
|
path?: string
|
||||||
|
): Promise<Readable | ReadableStream<any> | Blob> {
|
||||||
|
if (path)
|
||||||
|
path = path.startsWith('/') ? path.replace('/', '') : `${path}`;
|
||||||
|
|
||||||
|
const key: string = path ? `${path}/${filename}` : filename;
|
||||||
|
const input: GetObjectCommandInput = {
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: key,
|
||||||
|
};
|
||||||
|
const command: GetObjectCommand = new GetObjectCommand(input);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item: GetObjectOutput = await this.s3Client.send(command);
|
||||||
|
|
||||||
|
return item.Body;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async putItemInBucket(
|
||||||
|
filename: string,
|
||||||
|
content:
|
||||||
|
| string
|
||||||
|
| Uint8Array
|
||||||
|
| Buffer
|
||||||
|
| Readable
|
||||||
|
| ReadableStream
|
||||||
|
| Blob,
|
||||||
|
options?: IAwsS3PutItemOptions
|
||||||
|
): Promise<AwsS3Serialization> {
|
||||||
|
let path: string = options?.path;
|
||||||
|
const acl: string = options?.acl ? options.acl : 'public-read';
|
||||||
|
|
||||||
|
if (path)
|
||||||
|
path = path.startsWith('/') ? path.replace('/', '') : `${path}`;
|
||||||
|
|
||||||
|
const mime: string = filename
|
||||||
|
.substring(filename.lastIndexOf('.') + 1, filename.length)
|
||||||
|
.toUpperCase();
|
||||||
|
const key: string = path ? `${path}/${filename}` : filename;
|
||||||
|
const command: PutObjectCommand = new PutObjectCommand({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: key,
|
||||||
|
Body: content,
|
||||||
|
ACL: acl,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.s3Client.send(command);
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
pathWithFilename: key,
|
||||||
|
filename: filename,
|
||||||
|
completedUrl: `${this.baseUrl}/${key}`,
|
||||||
|
baseUrl: this.baseUrl,
|
||||||
|
mime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteItemInBucket(filename: string): Promise<void> {
|
||||||
|
const command: DeleteObjectCommand = new DeleteObjectCommand({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: filename,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.s3Client.send(command);
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteItemsInBucket(filenames: string[]): Promise<void> {
|
||||||
|
const keys: ObjectIdentifier[] = filenames.map((val) => ({
|
||||||
|
Key: val,
|
||||||
|
}));
|
||||||
|
const command: DeleteObjectsCommand = new DeleteObjectsCommand({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Delete: {
|
||||||
|
Objects: keys,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.s3Client.send(command);
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFolder(dir: string): Promise<void> {
|
||||||
|
const commandList: ListObjectsV2Command = new ListObjectsV2Command({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Prefix: dir,
|
||||||
|
});
|
||||||
|
const lists = await this.s3Client.send(commandList);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listItems = lists.Contents.map((val) => ({
|
||||||
|
Key: val.Key,
|
||||||
|
}));
|
||||||
|
const commandDeleteItems: DeleteObjectsCommand =
|
||||||
|
new DeleteObjectsCommand({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Delete: {
|
||||||
|
Objects: listItems,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.s3Client.send(commandDeleteItems);
|
||||||
|
|
||||||
|
const commandDelete: DeleteObjectCommand = new DeleteObjectCommand({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: dir,
|
||||||
|
});
|
||||||
|
await this.s3Client.send(commandDelete);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMultiPart(
|
||||||
|
filename: string,
|
||||||
|
options?: IAwsS3PutItemOptions
|
||||||
|
): Promise<AwsS3MultipartSerialization> {
|
||||||
|
let path: string = options?.path;
|
||||||
|
const acl: string = options?.acl ? options.acl : 'public-read';
|
||||||
|
|
||||||
|
if (path)
|
||||||
|
path = path.startsWith('/') ? path.replace('/', '') : `${path}`;
|
||||||
|
|
||||||
|
const mime: string = filename
|
||||||
|
.substring(filename.lastIndexOf('.') + 1, filename.length)
|
||||||
|
.toUpperCase();
|
||||||
|
const key: string = path ? `${path}/${filename}` : filename;
|
||||||
|
|
||||||
|
const multiPartInput: CreateMultipartUploadCommandInput = {
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: key,
|
||||||
|
ACL: acl,
|
||||||
|
};
|
||||||
|
const multiPartCommand: CreateMultipartUploadCommand =
|
||||||
|
new CreateMultipartUploadCommand(multiPartInput);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.s3Client.send(multiPartCommand);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadId: response.UploadId,
|
||||||
|
path,
|
||||||
|
pathWithFilename: key,
|
||||||
|
filename: filename,
|
||||||
|
completedUrl: `${this.baseUrl}/${key}`,
|
||||||
|
baseUrl: this.baseUrl,
|
||||||
|
mime,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadPart(
|
||||||
|
path: string,
|
||||||
|
content: UploadPartRequest['Body'] | string | Uint8Array | Buffer,
|
||||||
|
uploadId: string,
|
||||||
|
partNumber: number
|
||||||
|
): Promise<AwsS3MultipartPartsSerialization> {
|
||||||
|
const uploadPartInput: UploadPartCommandInput = {
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: path,
|
||||||
|
Body: content,
|
||||||
|
PartNumber: partNumber,
|
||||||
|
UploadId: uploadId,
|
||||||
|
};
|
||||||
|
const uploadPartCommand: UploadPartCommand = new UploadPartCommand(
|
||||||
|
uploadPartInput
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { ETag } = await this.s3Client.send(uploadPartCommand);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ETag,
|
||||||
|
PartNumber: partNumber,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeMultipart(
|
||||||
|
path: string,
|
||||||
|
uploadId: string,
|
||||||
|
parts: CompletedPart[]
|
||||||
|
): Promise<void> {
|
||||||
|
const completeMultipartInput: CompleteMultipartUploadCommandInput = {
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: path,
|
||||||
|
UploadId: uploadId,
|
||||||
|
MultipartUpload: {
|
||||||
|
Parts: parts,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const completeMultipartCommand: CompleteMultipartUploadCommand =
|
||||||
|
new CompleteMultipartUploadCommand(completeMultipartInput);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.s3Client.send(completeMultipartCommand);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async abortMultipart(path: string, uploadId: string): Promise<void> {
|
||||||
|
const abortMultipartInput: AbortMultipartUploadCommandInput = {
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Key: path,
|
||||||
|
UploadId: uploadId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortMultipartCommand: AbortMultipartUploadCommand =
|
||||||
|
new AbortMultipartUploadCommand(abortMultipartInput);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.s3Client.send(abortMultipartCommand);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (err: any) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/common/common.module.ts
Normal file
179
src/common/common.module.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { DebuggerModule } from 'src/common/debugger/debugger.module';
|
||||||
|
import { HelperModule } from 'src/common/helper/helper.module';
|
||||||
|
import { ErrorModule } from 'src/common/error/error.module';
|
||||||
|
import { ResponseModule } from 'src/common/response/response.module';
|
||||||
|
import { RequestModule } from 'src/common/request/request.module';
|
||||||
|
import { AuthModule } from 'src/common/auth/auth.module';
|
||||||
|
import { MessageModule } from 'src/common/message/message.module';
|
||||||
|
import { LoggerModule } from 'src/common/logger/logger.module';
|
||||||
|
import { PaginationModule } from 'src/common/pagination/pagination.module';
|
||||||
|
import Joi from 'joi';
|
||||||
|
import { ENUM_MESSAGE_LANGUAGE } from './message/constants/message.enum.constant';
|
||||||
|
import configs from 'src/configs';
|
||||||
|
import { SettingModule } from 'src/common/setting/setting.module';
|
||||||
|
import { ApiKeyModule } from 'src/common/api-key/api-key.module';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import { DatabaseOptionsService } from 'src/common/database/services/database.options.service';
|
||||||
|
import { DatabaseOptionsModule } from 'src/common/database/database.options.module';
|
||||||
|
import { DATABASE_CONNECTION_NAME } from 'src/common/database/constants/database.constant';
|
||||||
|
import { ENUM_APP_ENVIRONMENT } from 'src/app/constants/app.enum.constant';
|
||||||
|
import { APP_LANGUAGE } from 'src/app/constants/app.constant';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [],
|
||||||
|
providers: [],
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
load: configs,
|
||||||
|
isGlobal: true,
|
||||||
|
cache: true,
|
||||||
|
envFilePath: ['.env'],
|
||||||
|
expandVariables: true,
|
||||||
|
validationSchema: Joi.object({
|
||||||
|
APP_NAME: Joi.string().required(),
|
||||||
|
APP_ENV: Joi.string()
|
||||||
|
.valid(...Object.values(ENUM_APP_ENVIRONMENT))
|
||||||
|
.default('development')
|
||||||
|
.required(),
|
||||||
|
APP_LANGUAGE: Joi.string()
|
||||||
|
.valid(...Object.values(ENUM_MESSAGE_LANGUAGE))
|
||||||
|
.default(APP_LANGUAGE)
|
||||||
|
.required(),
|
||||||
|
|
||||||
|
HTTP_ENABLE: Joi.boolean().default(true).required(),
|
||||||
|
HTTP_HOST: [
|
||||||
|
Joi.string().ip({ version: 'ipv4' }).required(),
|
||||||
|
Joi.valid('localhost').required(),
|
||||||
|
],
|
||||||
|
HTTP_PORT: Joi.number().default(3000).required(),
|
||||||
|
HTTP_VERSIONING_ENABLE: Joi.boolean().default(true).required(),
|
||||||
|
HTTP_VERSION: Joi.number().required(),
|
||||||
|
|
||||||
|
DEBUGGER_HTTP_WRITE_INTO_FILE: Joi.boolean()
|
||||||
|
.default(false)
|
||||||
|
.required(),
|
||||||
|
DEBUGGER_HTTP_WRITE_INTO_CONSOLE: Joi.boolean()
|
||||||
|
.default(false)
|
||||||
|
.required(),
|
||||||
|
DEBUGGER_SYSTEM_WRITE_INTO_FILE: Joi.boolean()
|
||||||
|
.default(false)
|
||||||
|
.required(),
|
||||||
|
DEBUGGER_SYSTEM_WRITE_INTO_CONSOLE: Joi.boolean()
|
||||||
|
.default(false)
|
||||||
|
.required(),
|
||||||
|
|
||||||
|
JOB_ENABLE: Joi.boolean().default(false).required(),
|
||||||
|
|
||||||
|
DATABASE_HOST: Joi.string()
|
||||||
|
.default('mongodb://localhost:27017')
|
||||||
|
.required(),
|
||||||
|
DATABASE_NAME: Joi.string().default('ack').required(),
|
||||||
|
DATABASE_USER: Joi.string().allow(null, '').optional(),
|
||||||
|
DATABASE_PASSWORD: Joi.string().allow(null, '').optional(),
|
||||||
|
DATABASE_DEBUG: Joi.boolean().default(false).required(),
|
||||||
|
DATABASE_OPTIONS: Joi.string().allow(null, '').optional(),
|
||||||
|
|
||||||
|
AUTH_JWT_SUBJECT: Joi.string().required(),
|
||||||
|
AUTH_JWT_AUDIENCE: Joi.string().required(),
|
||||||
|
AUTH_JWT_ISSUER: Joi.string().required(),
|
||||||
|
|
||||||
|
AUTH_JWT_ACCESS_TOKEN_SECRET_KEY: Joi.string()
|
||||||
|
.alphanum()
|
||||||
|
.min(5)
|
||||||
|
.max(50)
|
||||||
|
.required(),
|
||||||
|
AUTH_JWT_ACCESS_TOKEN_EXPIRED: Joi.string()
|
||||||
|
.default('15m')
|
||||||
|
.required(),
|
||||||
|
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_SECRET_KEY: Joi.string()
|
||||||
|
.alphanum()
|
||||||
|
.min(5)
|
||||||
|
.max(50)
|
||||||
|
.required(),
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_EXPIRED: Joi.string()
|
||||||
|
.default('7d')
|
||||||
|
.required(),
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_REMEMBER_ME_EXPIRED: Joi.string()
|
||||||
|
.default('30d')
|
||||||
|
.required(),
|
||||||
|
AUTH_JWT_REFRESH_TOKEN_NOT_BEFORE_EXPIRATION: Joi.string()
|
||||||
|
.default('15m')
|
||||||
|
.required(),
|
||||||
|
|
||||||
|
AUTH_PERMISSION_TOKEN_SECRET_KEY: Joi.string()
|
||||||
|
.alphanum()
|
||||||
|
.min(5)
|
||||||
|
.max(50)
|
||||||
|
.required(),
|
||||||
|
AUTH_PERMISSION_TOKEN_EXPIRED: Joi.string()
|
||||||
|
.default('5m')
|
||||||
|
.required(),
|
||||||
|
|
||||||
|
AUTH_JWT_PAYLOAD_ENCRYPT: Joi.boolean()
|
||||||
|
.default(false)
|
||||||
|
.required(),
|
||||||
|
AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_KEY: Joi.string()
|
||||||
|
.allow(null, '')
|
||||||
|
.min(20)
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
AUTH_JWT_PAYLOAD_ACCESS_TOKEN_ENCRYPT_IV: Joi.string()
|
||||||
|
.allow(null, '')
|
||||||
|
.min(16)
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_KEY: Joi.string()
|
||||||
|
.allow(null, '')
|
||||||
|
.min(20)
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
AUTH_JWT_PAYLOAD_REFRESH_TOKEN_ENCRYPT_IV: Joi.string()
|
||||||
|
.allow(null, '')
|
||||||
|
.min(16)
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_KEY: Joi.string()
|
||||||
|
.allow(null, '')
|
||||||
|
.min(20)
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
AUTH_PAYLOAD_PERMISSION_TOKEN_ENCRYPT_IV: Joi.string()
|
||||||
|
.allow(null, '')
|
||||||
|
.min(16)
|
||||||
|
.max(50)
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
AWS_CREDENTIAL_KEY: Joi.string().allow(null, '').optional(),
|
||||||
|
AWS_CREDENTIAL_SECRET: Joi.string().allow(null, '').optional(),
|
||||||
|
AWS_S3_REGION: Joi.string().allow(null, '').optional(),
|
||||||
|
AWS_S3_BUCKET: Joi.string().allow(null, '').optional(),
|
||||||
|
}),
|
||||||
|
validationOptions: {
|
||||||
|
allowUnknown: true,
|
||||||
|
abortEarly: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
MongooseModule.forRootAsync({
|
||||||
|
connectionName: DATABASE_CONNECTION_NAME,
|
||||||
|
imports: [DatabaseOptionsModule],
|
||||||
|
inject: [DatabaseOptionsService],
|
||||||
|
useFactory: (databaseOptionsService: DatabaseOptionsService) =>
|
||||||
|
databaseOptionsService.createOptions(),
|
||||||
|
}),
|
||||||
|
MessageModule,
|
||||||
|
HelperModule,
|
||||||
|
PaginationModule,
|
||||||
|
ErrorModule,
|
||||||
|
DebuggerModule.forRoot(),
|
||||||
|
ResponseModule,
|
||||||
|
RequestModule,
|
||||||
|
SettingModule,
|
||||||
|
LoggerModule,
|
||||||
|
ApiKeyModule,
|
||||||
|
AuthModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CommonModule {}
|
||||||
21
src/common/dashboard/constants/dashboard.doc.constant.ts
Normal file
21
src/common/dashboard/constants/dashboard.doc.constant.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
|
||||||
|
export const DashboardDocQueryStartDate = [
|
||||||
|
{
|
||||||
|
name: 'startDate',
|
||||||
|
allowEmptyValue: true,
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
example: faker.date.recent().toString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DashboardDocQueryEndDate = [
|
||||||
|
{
|
||||||
|
name: 'endDate',
|
||||||
|
allowEmptyValue: true,
|
||||||
|
required: false,
|
||||||
|
type: 'string',
|
||||||
|
example: faker.date.recent().toString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
10
src/common/dashboard/dashboard.module.ts
Normal file
10
src/common/dashboard/dashboard.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DashboardService } from 'src/common/dashboard/services/dashboard.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [],
|
||||||
|
providers: [DashboardService],
|
||||||
|
exports: [DashboardService],
|
||||||
|
imports: [],
|
||||||
|
})
|
||||||
|
export class DashboardModule {}
|
||||||
29
src/common/dashboard/dtos/dashboard.ts
Normal file
29
src/common/dashboard/dtos/dashboard.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsDate, IsOptional, ValidateIf } from 'class-validator';
|
||||||
|
import { MinGreaterThan } from 'src/common/request/validations/request.min-greater-than.validation';
|
||||||
|
|
||||||
|
export class DashboardDto {
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'startDate',
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsDate()
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Date)
|
||||||
|
@ValidateIf((e) => e.startDate !== '' || e.endDate !== '')
|
||||||
|
startDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'endDate',
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
@IsDate()
|
||||||
|
@IsOptional()
|
||||||
|
@MinGreaterThan('startDate')
|
||||||
|
@Type(() => Date)
|
||||||
|
@ValidateIf((e) => e.startDate !== '' || e.endDate !== '')
|
||||||
|
endDate?: Date;
|
||||||
|
}
|
||||||
18
src/common/dashboard/interfaces/dashboard.interface.ts
Normal file
18
src/common/dashboard/interfaces/dashboard.interface.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface IDashboardStartAndEndDate {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDashboardStartAndEndYear {
|
||||||
|
startYear: number;
|
||||||
|
endYear: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDashboardStartAndEnd {
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDashboardMonthAndYear extends Partial<IDashboardStartAndEnd> {
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { DashboardDto } from 'src/common/dashboard/dtos/dashboard';
|
||||||
|
import {
|
||||||
|
IDashboardStartAndEnd,
|
||||||
|
IDashboardStartAndEndDate,
|
||||||
|
IDashboardStartAndEndYear,
|
||||||
|
} from 'src/common/dashboard/interfaces/dashboard.interface';
|
||||||
|
|
||||||
|
export interface IDashboardService {
|
||||||
|
getStartAndEndDate(date: DashboardDto): Promise<IDashboardStartAndEndDate>;
|
||||||
|
|
||||||
|
getMonths(): Promise<number[]>;
|
||||||
|
|
||||||
|
getStartAndEndYear({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}: IDashboardStartAndEndDate): Promise<IDashboardStartAndEndYear>;
|
||||||
|
|
||||||
|
getStartAndEndMonth({
|
||||||
|
month,
|
||||||
|
year,
|
||||||
|
}: IDashboardStartAndEnd): Promise<IDashboardStartAndEndDate>;
|
||||||
|
|
||||||
|
getPercentage(value: number, total: number): Promise<number>;
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { ApiProperty, OmitType } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class DashboardMonthAndYearSerialization {
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'total',
|
||||||
|
example: 12,
|
||||||
|
})
|
||||||
|
month: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'total',
|
||||||
|
example: 2001,
|
||||||
|
})
|
||||||
|
year: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'total',
|
||||||
|
description: 'total of target',
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
example: 0,
|
||||||
|
})
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashboardMonthAndYearPercentageSerialization extends OmitType(
|
||||||
|
DashboardMonthAndYearSerialization,
|
||||||
|
['total'] as const
|
||||||
|
) {
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'percent',
|
||||||
|
description: 'Percent of target',
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
example: 15.4,
|
||||||
|
})
|
||||||
|
percent: number;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class DashboardSerialization {
|
||||||
|
@ApiProperty({
|
||||||
|
name: 'total',
|
||||||
|
example: faker.random.numeric(4, { allowLeadingZeros: false }),
|
||||||
|
description: 'Total user',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
81
src/common/dashboard/services/dashboard.service.ts
Normal file
81
src/common/dashboard/services/dashboard.service.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { DashboardDto } from 'src/common/dashboard/dtos/dashboard';
|
||||||
|
import {
|
||||||
|
IDashboardStartAndEnd,
|
||||||
|
IDashboardStartAndEndDate,
|
||||||
|
IDashboardStartAndEndYear,
|
||||||
|
} from 'src/common/dashboard/interfaces/dashboard.interface';
|
||||||
|
import { IDashboardService } from 'src/common/dashboard/interfaces/dashboard.service.interface';
|
||||||
|
import { HelperDateService } from 'src/common/helper/services/helper.date.service';
|
||||||
|
import { HelperNumberService } from 'src/common/helper/services/helper.number.service';
|
||||||
|
|
||||||
|
export class DashboardService implements IDashboardService {
|
||||||
|
constructor(
|
||||||
|
private readonly helperDateService: HelperDateService,
|
||||||
|
private readonly helperNumberService: HelperNumberService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getStartAndEndDate(
|
||||||
|
date: DashboardDto
|
||||||
|
): Promise<IDashboardStartAndEndDate> {
|
||||||
|
const today = this.helperDateService.create();
|
||||||
|
let { startDate, endDate } = date;
|
||||||
|
|
||||||
|
if (!startDate && !endDate) {
|
||||||
|
startDate = this.helperDateService.startOfYear(today);
|
||||||
|
endDate = this.helperDateService.endOfYear(today);
|
||||||
|
} else {
|
||||||
|
if (!startDate) {
|
||||||
|
startDate = this.helperDateService.startOfDay();
|
||||||
|
} else {
|
||||||
|
startDate = this.helperDateService.startOfDay(startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!endDate) {
|
||||||
|
endDate = this.helperDateService.endOfDay();
|
||||||
|
} else {
|
||||||
|
endDate = this.helperDateService.endOfDay(endDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMonths(): Promise<number[]> {
|
||||||
|
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStartAndEndYear({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}: IDashboardStartAndEndDate): Promise<IDashboardStartAndEndYear> {
|
||||||
|
return {
|
||||||
|
startYear: startDate.getFullYear(),
|
||||||
|
endYear: endDate.getFullYear(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStartAndEndMonth({
|
||||||
|
month,
|
||||||
|
year,
|
||||||
|
}: IDashboardStartAndEnd): Promise<IDashboardStartAndEndDate> {
|
||||||
|
const monthString = `${month}`.padStart(2, '0');
|
||||||
|
const date: Date = this.helperDateService.create(
|
||||||
|
`${year}-${monthString}-01`
|
||||||
|
);
|
||||||
|
|
||||||
|
const startDate = this.helperDateService.startOfMonth(date);
|
||||||
|
const endDate = this.helperDateService.endOfMonth(date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPercentage(value: number, total: number): Promise<number> {
|
||||||
|
return this.helperNumberService.percent(value, total);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
DATABASE_CREATED_AT_FIELD_NAME,
|
||||||
|
DATABASE_DELETED_AT_FIELD_NAME,
|
||||||
|
DATABASE_UPDATED_AT_FIELD_NAME,
|
||||||
|
} from 'src/common/database/constants/database.constant';
|
||||||
|
|
||||||
|
export abstract class DatabaseBaseEntityAbstract<T = any> {
|
||||||
|
abstract _id: T;
|
||||||
|
abstract [DATABASE_DELETED_AT_FIELD_NAME]?: Date;
|
||||||
|
abstract [DATABASE_CREATED_AT_FIELD_NAME]?: Date;
|
||||||
|
abstract [DATABASE_UPDATED_AT_FIELD_NAME]?: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
IDatabaseCreateOptions,
|
||||||
|
IDatabaseExistOptions,
|
||||||
|
IDatabaseFindAllOptions,
|
||||||
|
IDatabaseFindOneOptions,
|
||||||
|
IDatabaseOptions,
|
||||||
|
IDatabaseCreateManyOptions,
|
||||||
|
IDatabaseManyOptions,
|
||||||
|
IDatabaseSoftDeleteManyOptions,
|
||||||
|
IDatabaseRestoreManyOptions,
|
||||||
|
IDatabaseRawOptions,
|
||||||
|
} from 'src/common/database/interfaces/database.interface';
|
||||||
|
|
||||||
|
export abstract class DatabaseBaseRepositoryAbstract<Entity> {
|
||||||
|
abstract findAll<T = Entity>(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions<any>
|
||||||
|
): Promise<T[]>;
|
||||||
|
|
||||||
|
abstract findAllDistinct<T = Entity>(
|
||||||
|
fieldDistinct: string,
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions<any>
|
||||||
|
): Promise<T[]>;
|
||||||
|
|
||||||
|
abstract findOne<T = Entity>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions<any>
|
||||||
|
): Promise<T>;
|
||||||
|
|
||||||
|
abstract findOneById<T = Entity>(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions<any>
|
||||||
|
): Promise<T>;
|
||||||
|
|
||||||
|
abstract findOneAndLock<T = Entity>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions<any>
|
||||||
|
): Promise<T>;
|
||||||
|
|
||||||
|
abstract findOneByIdAndLock<T = Entity>(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions<any>
|
||||||
|
): Promise<T>;
|
||||||
|
|
||||||
|
abstract getTotal(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseOptions<any>
|
||||||
|
): Promise<number>;
|
||||||
|
|
||||||
|
abstract exists(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseExistOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract create<Dto= any>(
|
||||||
|
data: Dto,
|
||||||
|
options?: IDatabaseCreateOptions<any>
|
||||||
|
): Promise<Entity>;
|
||||||
|
|
||||||
|
abstract save(repository: Entity): Promise<Entity>;
|
||||||
|
|
||||||
|
abstract delete(repository: Entity): Promise<Entity>;
|
||||||
|
|
||||||
|
abstract softDelete(repository: Entity): Promise<Entity>;
|
||||||
|
|
||||||
|
abstract restore(repository: Entity): Promise<Entity>;
|
||||||
|
|
||||||
|
abstract createMany<Dto>(
|
||||||
|
data: Dto[],
|
||||||
|
options?: IDatabaseCreateManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract deleteManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract deleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract softDeleteManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseSoftDeleteManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract softDeleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseSoftDeleteManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract restoreManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseRestoreManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract restoreMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseRestoreManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract updateMany<Dto>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
data: Dto,
|
||||||
|
options?: IDatabaseManyOptions<any>
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
abstract raw<RawResponse, RawQuery = any>(
|
||||||
|
rawOperation: RawQuery,
|
||||||
|
options?: IDatabaseRawOptions
|
||||||
|
): Promise<RawResponse[]>;
|
||||||
|
|
||||||
|
abstract model(): Promise<any>;
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { Prop } from '@nestjs/mongoose';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { DatabaseBaseEntityAbstract } from 'src/common/database/abstracts/database.base-entity.abstract';
|
||||||
|
import {
|
||||||
|
DATABASE_CREATED_AT_FIELD_NAME,
|
||||||
|
DATABASE_DELETED_AT_FIELD_NAME,
|
||||||
|
DATABASE_UPDATED_AT_FIELD_NAME,
|
||||||
|
} from 'src/common/database/constants/database.constant';
|
||||||
|
import { DatabaseDefaultObjectId } from 'src/common/database/constants/database.function.constant';
|
||||||
|
|
||||||
|
export abstract class DatabaseMongoObjectIdEntityAbstract extends DatabaseBaseEntityAbstract<Types.ObjectId> {
|
||||||
|
@Prop({
|
||||||
|
type: Types.ObjectId,
|
||||||
|
default: DatabaseDefaultObjectId,
|
||||||
|
})
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
index: true,
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]?: Date;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
index: 'asc',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
[DATABASE_CREATED_AT_FIELD_NAME]?: Date;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
index: 'desc',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
[DATABASE_UPDATED_AT_FIELD_NAME]?: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { Prop } from '@nestjs/mongoose';
|
||||||
|
import { DatabaseBaseEntityAbstract } from 'src/common/database/abstracts/database.base-entity.abstract';
|
||||||
|
import {
|
||||||
|
DATABASE_CREATED_AT_FIELD_NAME,
|
||||||
|
DATABASE_DELETED_AT_FIELD_NAME,
|
||||||
|
DATABASE_UPDATED_AT_FIELD_NAME,
|
||||||
|
} from 'src/common/database/constants/database.constant';
|
||||||
|
import { DatabaseDefaultUUID } from 'src/common/database/constants/database.function.constant';
|
||||||
|
|
||||||
|
export abstract class DatabaseMongoUUIDEntityAbstract extends DatabaseBaseEntityAbstract<string> {
|
||||||
|
@Prop({
|
||||||
|
type: String,
|
||||||
|
default: DatabaseDefaultUUID,
|
||||||
|
})
|
||||||
|
_id: string;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
index: true,
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]?: Date;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
index: 'asc',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
[DATABASE_CREATED_AT_FIELD_NAME]?: Date;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
required: false,
|
||||||
|
index: 'desc',
|
||||||
|
type: Date,
|
||||||
|
})
|
||||||
|
[DATABASE_UPDATED_AT_FIELD_NAME]?: Date;
|
||||||
|
}
|
||||||
@ -0,0 +1,746 @@
|
|||||||
|
import {
|
||||||
|
ClientSession,
|
||||||
|
Model,
|
||||||
|
PipelineStage,
|
||||||
|
PopulateOptions,
|
||||||
|
Types,
|
||||||
|
Document,
|
||||||
|
} from 'mongoose';
|
||||||
|
import { DatabaseBaseRepositoryAbstract } from 'src/common/database/abstracts/database.base-repository.abstract';
|
||||||
|
import { DATABASE_DELETED_AT_FIELD_NAME } from 'src/common/database/constants/database.constant';
|
||||||
|
import {
|
||||||
|
IDatabaseCreateOptions,
|
||||||
|
IDatabaseExistOptions,
|
||||||
|
IDatabaseFindAllOptions,
|
||||||
|
IDatabaseFindOneOptions,
|
||||||
|
IDatabaseOptions,
|
||||||
|
IDatabaseCreateManyOptions,
|
||||||
|
IDatabaseManyOptions,
|
||||||
|
IDatabaseSoftDeleteManyOptions,
|
||||||
|
IDatabaseRestoreManyOptions,
|
||||||
|
IDatabaseRawOptions,
|
||||||
|
} from 'src/common/database/interfaces/database.interface';
|
||||||
|
|
||||||
|
export abstract class DatabaseMongoObjectIdRepositoryAbstract<
|
||||||
|
Entity,
|
||||||
|
EntityDocument
|
||||||
|
> extends DatabaseBaseRepositoryAbstract<EntityDocument> {
|
||||||
|
protected _repository: Model<Entity>;
|
||||||
|
protected _joinOnFind?: PopulateOptions | PopulateOptions[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
repository: Model<Entity>,
|
||||||
|
options?: PopulateOptions | PopulateOptions[]
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._repository = repository;
|
||||||
|
this._joinOnFind = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll<T = Entity>(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions<ClientSession>
|
||||||
|
): Promise<T[]> {
|
||||||
|
const findAll = this._repository.find<Entity>(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findAll.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findAll.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.paging) {
|
||||||
|
findAll.limit(options.paging.limit).skip(options.paging.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findAll.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findAll.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findAll.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findAll.lean() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllDistinct<T = Entity>(
|
||||||
|
fieldDistinct: string,
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions<ClientSession>
|
||||||
|
): Promise<T[]> {
|
||||||
|
const findAll = this._repository.distinct<Entity>(
|
||||||
|
fieldDistinct,
|
||||||
|
find
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findAll.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findAll.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.paging) {
|
||||||
|
findAll.limit(options.paging.limit).skip(options.paging.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findAll.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findAll.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findAll.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findAll.lean() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne<T = EntityDocument>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findOne<EntityDocument>(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findOne.exec() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneById<T = EntityDocument>(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findById<EntityDocument>(
|
||||||
|
new Types.ObjectId(_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findOne.exec() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneAndLock<T = EntityDocument>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findOneAndUpdate<EntityDocument>(
|
||||||
|
find,
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
useFindAndModify: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findOne.exec() as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByIdAndLock<T = EntityDocument>(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findByIdAndUpdate(
|
||||||
|
new Types.ObjectId(_id),
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
useFindAndModify: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findOne.exec() as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTotal(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseOptions<ClientSession>
|
||||||
|
): Promise<number> {
|
||||||
|
const count = this._repository.countDocuments(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
count.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
count.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
count.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
count.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseExistOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
|
||||||
|
if (options?.excludeId) {
|
||||||
|
find= {
|
||||||
|
...find,
|
||||||
|
_id: {
|
||||||
|
$nin: options?.excludeId.map(
|
||||||
|
(val) => new Types.ObjectId(val)
|
||||||
|
) ?? [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exist = this._repository.exists(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
exist.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
exist.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
exist.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
exist.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await exist;
|
||||||
|
return result ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create<Dto = any>(
|
||||||
|
data: Dto,
|
||||||
|
options?: IDatabaseCreateOptions<ClientSession>
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
const dataCreate: Record<string, any> = data;
|
||||||
|
dataCreate._id = new Types.ObjectId(options?._id);
|
||||||
|
|
||||||
|
const created = await this._repository.create([dataCreate], {
|
||||||
|
session: options ? options.session : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return created[0] as EntityDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(
|
||||||
|
repository: EntityDocument & Document<Types.ObjectId>
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
return repository.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(
|
||||||
|
repository: EntityDocument & Document<Types.ObjectId>
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
return repository.deleteOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(
|
||||||
|
repository: EntityDocument &
|
||||||
|
Document<Types.ObjectId> & { deletedAt?: Date }
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
repository.deletedAt = new Date();
|
||||||
|
return repository.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(
|
||||||
|
repository: EntityDocument &
|
||||||
|
Document<Types.ObjectId> & { deletedAt?: Date }
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
repository.deletedAt = undefined;
|
||||||
|
return repository.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// bulk
|
||||||
|
async createMany<Dto>(
|
||||||
|
data: Dto[],
|
||||||
|
options?: IDatabaseCreateManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const dataCreate: Record<string, any>[] = data.map(
|
||||||
|
(val: Record<string, any>) => ({
|
||||||
|
...val,
|
||||||
|
_id: new Types.ObjectId(val._id),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const create = this._repository.insertMany(dataCreate, {
|
||||||
|
session: options ? options.session : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await create;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const del = this._repository.deleteMany({
|
||||||
|
_id: {
|
||||||
|
$in: _id.map((val) => new Types.ObjectId(val)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
del.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
del.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const del = this._repository.deleteMany(find);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
del.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
del.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDeleteManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseSoftDeleteManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const softDel = this._repository
|
||||||
|
.updateMany(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: _id.map((val) => new Types.ObjectId(val)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(false);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
softDel.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
softDel.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await softDel;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDeleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseSoftDeleteManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const softDel = this._repository
|
||||||
|
.updateMany(find, {
|
||||||
|
$set: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(false);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
softDel.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
softDel.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await softDel;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseRestoreManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const rest = this._repository
|
||||||
|
.updateMany(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: _id.map((val) => new Types.ObjectId(val)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
deletedAt: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(true);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
rest.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
rest.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rest;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseRestoreManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const rest = this._repository
|
||||||
|
.updateMany(find, {
|
||||||
|
$set: {
|
||||||
|
deletedAt: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(true);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
rest.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
rest.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rest;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMany<Dto>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
data: Dto,
|
||||||
|
options?: IDatabaseManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const update = this._repository
|
||||||
|
.updateMany(find, {
|
||||||
|
$set: data,
|
||||||
|
})
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(false);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
update.session(options.session as ClientSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
update.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await update;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// raw
|
||||||
|
async raw<RawResponse, RawQuery = PipelineStage[]>(
|
||||||
|
rawOperation: RawQuery,
|
||||||
|
options?: IDatabaseRawOptions
|
||||||
|
): Promise<RawResponse[]> {
|
||||||
|
if (!Array.isArray(rawOperation)) {
|
||||||
|
throw new Error('Must in array');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeline: PipelineStage[] = rawOperation;
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
pipeline.push({
|
||||||
|
$match: {
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: {
|
||||||
|
$exists: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pipeline.push({
|
||||||
|
$match: {
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggregate = this._repository.aggregate<RawResponse>(pipeline);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
aggregate.session(options?.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async model(): Promise<Model<Entity>> {
|
||||||
|
return this._repository;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,734 @@
|
|||||||
|
import {
|
||||||
|
ClientSession,
|
||||||
|
Model,
|
||||||
|
PipelineStage,
|
||||||
|
PopulateOptions,
|
||||||
|
Document,
|
||||||
|
} from 'mongoose';
|
||||||
|
import { DatabaseBaseRepositoryAbstract } from 'src/common/database/abstracts/database.base-repository.abstract';
|
||||||
|
import { DATABASE_DELETED_AT_FIELD_NAME } from 'src/common/database/constants/database.constant';
|
||||||
|
import {
|
||||||
|
IDatabaseCreateOptions,
|
||||||
|
IDatabaseExistOptions,
|
||||||
|
IDatabaseFindAllOptions,
|
||||||
|
IDatabaseFindOneOptions,
|
||||||
|
IDatabaseOptions,
|
||||||
|
IDatabaseCreateManyOptions,
|
||||||
|
IDatabaseManyOptions,
|
||||||
|
IDatabaseSoftDeleteManyOptions,
|
||||||
|
IDatabaseRestoreManyOptions,
|
||||||
|
IDatabaseRawOptions,
|
||||||
|
} from 'src/common/database/interfaces/database.interface';
|
||||||
|
|
||||||
|
export abstract class DatabaseMongoUUIDRepositoryAbstract<
|
||||||
|
Entity,
|
||||||
|
EntityDocument
|
||||||
|
> extends DatabaseBaseRepositoryAbstract<EntityDocument> {
|
||||||
|
protected _repository: Model<Entity>;
|
||||||
|
protected _joinOnFind?: PopulateOptions | PopulateOptions[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
repository: Model<Entity>,
|
||||||
|
options?: PopulateOptions | PopulateOptions[]
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._repository = repository;
|
||||||
|
this._joinOnFind = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll<T = EntityDocument>(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions<ClientSession>
|
||||||
|
): Promise<T[]> {
|
||||||
|
const findAll = this._repository.find<EntityDocument>(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findAll.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findAll.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.paging) {
|
||||||
|
findAll.limit(options.paging.limit).skip(options.paging.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findAll.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findAll.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findAll.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return findAll.lean() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllDistinct<T = EntityDocument>(
|
||||||
|
fieldDistinct: string,
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseFindAllOptions<ClientSession>
|
||||||
|
): Promise<T[]> {
|
||||||
|
const findAll = this._repository.distinct<EntityDocument>(
|
||||||
|
fieldDistinct,
|
||||||
|
find
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findAll.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findAll.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findAll.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.paging) {
|
||||||
|
findAll.limit(options.paging.limit).skip(options.paging.offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findAll.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findAll.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findAll.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findAll.lean() as any;
|
||||||
|
}
|
||||||
|
async findOne<T = EntityDocument>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findOne<EntityDocument>(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findOne.exec() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneById<T = EntityDocument>(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findById<EntityDocument>(_id);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return findOne.exec() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneAndLock<T = EntityDocument>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findOneAndUpdate<EntityDocument>(
|
||||||
|
find,
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
useFindAndModify: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findOne.exec() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneByIdAndLock<T = EntityDocument>(
|
||||||
|
_id: string,
|
||||||
|
options?: IDatabaseFindOneOptions<ClientSession>
|
||||||
|
): Promise<T> {
|
||||||
|
const findOne = this._repository.findByIdAndUpdate<EntityDocument>(
|
||||||
|
_id,
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
useFindAndModify: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
findOne.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
findOne.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.select) {
|
||||||
|
findOne.select(options.select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
findOne.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
findOne.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.order) {
|
||||||
|
findOne.sort(options.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findOne.exec() as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTotal(
|
||||||
|
find?: Record<string, any>,
|
||||||
|
options?: IDatabaseOptions<ClientSession>
|
||||||
|
): Promise<number> {
|
||||||
|
const count = this._repository.countDocuments(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
count.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
count.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
count.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
count.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseExistOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
|
||||||
|
if (options?.excludeId) {
|
||||||
|
find = {
|
||||||
|
...find,
|
||||||
|
_id: {
|
||||||
|
$nin: options?.excludeId ?? [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const exist = this._repository.exists(find);
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
exist.or([
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
exist.where(DATABASE_DELETED_AT_FIELD_NAME).exists(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
exist.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
exist.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await exist;
|
||||||
|
return result ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create<Dto = any>(
|
||||||
|
data: Dto,
|
||||||
|
options?: IDatabaseCreateOptions<ClientSession>
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
const dataCreate: Record<string, any> = data;
|
||||||
|
if (options?._id) {
|
||||||
|
dataCreate._id = options._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await this._repository.create([dataCreate], {
|
||||||
|
session: options ? options.session : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return created[0] as EntityDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(
|
||||||
|
repository: EntityDocument & Document<string>
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
return repository.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(
|
||||||
|
repository: EntityDocument & Document<string>
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
return repository.deleteOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(
|
||||||
|
repository: EntityDocument & Document<string> & { deletedAt?: Date }
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
repository.deletedAt = new Date();
|
||||||
|
return repository.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(
|
||||||
|
repository: EntityDocument & Document<string> & { deletedAt?: Date }
|
||||||
|
): Promise<EntityDocument> {
|
||||||
|
repository.deletedAt = undefined;
|
||||||
|
return repository.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// bulk
|
||||||
|
async createMany<Dto>(
|
||||||
|
data: Dto[],
|
||||||
|
options?: IDatabaseCreateManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const create = this._repository.insertMany(data, {
|
||||||
|
session: options ? options.session : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await create;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const del = this._repository.deleteMany({
|
||||||
|
_id: {
|
||||||
|
$in: _id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
del.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
del.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const del = this._repository.deleteMany(find);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
del.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
del.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDeleteManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseSoftDeleteManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const softDel = this._repository
|
||||||
|
.updateMany(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: _id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(false);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
softDel.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
softDel.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await softDel;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDeleteMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseSoftDeleteManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const softDel = this._repository
|
||||||
|
.updateMany(find, {
|
||||||
|
$set: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(false);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
softDel.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
softDel.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await softDel;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreManyByIds(
|
||||||
|
_id: string[],
|
||||||
|
options?: IDatabaseRestoreManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const rest = this._repository
|
||||||
|
.updateMany(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: _id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
deletedAt: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(true);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
rest.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
rest.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rest;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreMany(
|
||||||
|
find: Record<string, any>,
|
||||||
|
options?: IDatabaseRestoreManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const rest = this._repository
|
||||||
|
.updateMany(find, {
|
||||||
|
$set: {
|
||||||
|
deletedAt: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(true);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
rest.session(options.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
rest.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rest;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMany<Dto>(
|
||||||
|
find: Record<string, any>,
|
||||||
|
data: Dto,
|
||||||
|
options?: IDatabaseManyOptions<ClientSession>
|
||||||
|
): Promise<boolean> {
|
||||||
|
const update = this._repository
|
||||||
|
.updateMany(find, {
|
||||||
|
$set: data,
|
||||||
|
})
|
||||||
|
.where(DATABASE_DELETED_AT_FIELD_NAME)
|
||||||
|
.exists(false);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
update.session(options.session as ClientSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.join) {
|
||||||
|
update.populate(
|
||||||
|
typeof options.join === 'boolean'
|
||||||
|
? this._joinOnFind
|
||||||
|
: (options.join as PopulateOptions | PopulateOptions[])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await update;
|
||||||
|
return true;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async raw<RawResponse, RawQuery = PipelineStage[]>(
|
||||||
|
rawOperation: RawQuery,
|
||||||
|
options?: IDatabaseRawOptions
|
||||||
|
): Promise<RawResponse[]> {
|
||||||
|
if (!Array.isArray(rawOperation)) {
|
||||||
|
throw new Error('Must in array');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipeline: PipelineStage[] = rawOperation;
|
||||||
|
|
||||||
|
if (options?.withDeleted) {
|
||||||
|
pipeline.push({
|
||||||
|
$match: {
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: {
|
||||||
|
$exists: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pipeline.push({
|
||||||
|
$match: {
|
||||||
|
[DATABASE_DELETED_AT_FIELD_NAME]: { $exists: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggregate = this._repository.aggregate<RawResponse>(pipeline);
|
||||||
|
|
||||||
|
if (options?.session) {
|
||||||
|
aggregate.session(options?.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregate;
|
||||||
|
}
|
||||||
|
|
||||||
|
async model(): Promise<Model<Entity>> {
|
||||||
|
return this._repository;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user