mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-10-23 11:17:56 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9db620d818 | ||
|
|
89982f851f | ||
|
|
8bc3a307c0 | ||
|
|
501deaac0b | ||
|
|
5d386322e4 | ||
|
|
ecb058209d |
@@ -1,31 +0,0 @@
|
||||
NODE_ENV=dev
|
||||
PORT=5000
|
||||
DB_HOST=
|
||||
DB_PASS=
|
||||
DB_USER=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=
|
||||
MIGRATIONS_RUN=true
|
||||
SWAGGER_API_DOCS_PATH="/api-docs"
|
||||
|
||||
JWT_ACCESS_TOKEN_SECRET=your_access_token_secret
|
||||
JWT_ACCESS_TOKEN_EXPIRY=1d
|
||||
JWT_REFRESH_TOKEN_SECRET=your_refresh_token_secret
|
||||
JWT_REFRESH_TOKEN_EXPIRY=1d
|
||||
USE_MOCK=true
|
||||
|
||||
|
||||
OCI_TENANCY_ID=
|
||||
OCI_USER_ID=
|
||||
OCI_FINGERPRINT=
|
||||
OCI_PRIVATE_KEY=
|
||||
OCI_PASSPHRASE=""
|
||||
OCI_NAMESPACE=
|
||||
OCI_REGION=
|
||||
|
||||
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_USER=[email protected]
|
||||
MAIL_PASSWORD=
|
||||
MAIL_PORT=587
|
||||
MAIL_FROM=UBA
|
||||
@@ -1,47 +0,0 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:security/recommended'],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js', 'node_modules', 'dist', 'coverage', 'jest*\\.ts', 'typeorm*\\.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'func-names': ['error', 'as-needed'],
|
||||
'no-underscore-dangle': ['error'],
|
||||
'require-await': ['error'],
|
||||
'no-console': ['error'],
|
||||
'no-multi-assign': ['error'],
|
||||
'no-magic-numbers': ['error', { ignoreArrayIndexes: true }],
|
||||
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1, maxBOF: 0 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{ code: 120, tabWidth: 2, ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true },
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*spec.ts'],
|
||||
rules: {
|
||||
'no-magic-numbers': ['off'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
name: CI Zod
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
|
||||
env:
|
||||
NODE_ENV: development
|
||||
TAG: dev
|
||||
|
||||
jobs:
|
||||
install_dependencies:
|
||||
name: Install Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.4.1
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Cache Node Modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node_modules-
|
||||
|
||||
run_tests:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: install_dependencies
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.4.1
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Run Tests with Coverage
|
||||
run: npm run test:cov
|
||||
|
||||
build_app:
|
||||
name: Build Application
|
||||
runs-on: ubuntu-latest
|
||||
needs: run_tests
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.4.1
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build Application
|
||||
run: npm run build
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
+21
-52
@@ -1,55 +1,24 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
/static
|
||||
/media
|
||||
.idea/
|
||||
*.pyc
|
||||
media/
|
||||
*.name
|
||||
*.iml
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
*.xml
|
||||
*.pyo
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
.idea
|
||||
venv/*
|
||||
static/*
|
||||
*.pem
|
||||
*.sqlite3
|
||||
/migrations/__pycache__/
|
||||
/__pycache__/
|
||||
/*.pyc
|
||||
*/__pycache__/*.pyc
|
||||
__pycache__/
|
||||
*.env
|
||||
ve/*
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120
|
||||
}
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# Base image
|
||||
FROM node:22.4.1-alpine
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add ca-certificates curl && \
|
||||
update-ca-certificates
|
||||
|
||||
#saudi timezone
|
||||
ENV TZ=Asia/Riyadh
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir /app && chown node:node /app
|
||||
WORKDIR /app
|
||||
|
||||
# finally switch user to node
|
||||
USER node
|
||||
|
||||
|
||||
# A wildcard is used to ensure both package.json, package-lock.json and package-lock.json are copied
|
||||
COPY --chown=node:node package*.json ./
|
||||
|
||||
|
||||
|
||||
# Install app dependencies
|
||||
RUN npm install --frozen-lockfile
|
||||
# Bundle app source
|
||||
COPY --chown=node:node . .
|
||||
|
||||
# Creates a "dist" folder with the production build
|
||||
RUN npm run build
|
||||
|
||||
# Start the server using the production build
|
||||
CMD [ "node", "dist/main" ]
|
||||
@@ -1,99 +1 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
# ZODBank-Backend
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { Config as JestConfig } from 'jest';
|
||||
import { pathsToModuleNameMapper } from 'ts-jest';
|
||||
import { compilerOptions as tsConfigCompilerOptions } from './tsconfig.json';
|
||||
|
||||
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
|
||||
type JestProjectConfig = Exclude<ArrayElement<JestConfig['projects']>, string>;
|
||||
|
||||
const baseJestConfig: JestConfig = {
|
||||
// ...
|
||||
};
|
||||
const baseJestProjectConfig: JestProjectConfig = {
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.(t|j)s$': 'ts-jest',
|
||||
},
|
||||
moduleNameMapper: pathsToModuleNameMapper(tsConfigCompilerOptions.paths, { prefix: '<rootDir>' }),
|
||||
globalSetup: '<rootDir>/test/global-setup.ts',
|
||||
};
|
||||
const projects: { name?: string; rootDir: string }[] = [{ rootDir: '.' }];
|
||||
|
||||
export function buildJestConfig(config?: {
|
||||
jestConfigs?: JestConfig;
|
||||
jestProjectConfigs?: JestProjectConfig;
|
||||
}): JestConfig {
|
||||
return {
|
||||
...baseJestConfig,
|
||||
...config?.jestConfigs,
|
||||
projects: projects.map(({ name, rootDir }) => ({
|
||||
...baseJestProjectConfig,
|
||||
...config?.jestProjectConfigs,
|
||||
...(name && { displayName: name }),
|
||||
rootDir,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Config } from 'jest';
|
||||
import { buildJestConfig } from './jest.base.config';
|
||||
|
||||
const config: Config = buildJestConfig({
|
||||
jestProjectConfigs: {
|
||||
testRegex: '.*\\.spec\\.ts$',
|
||||
coveragePathIgnorePatterns: ['__testing__', 'entities', '<rootDir>/src/db', 'index.ts'],
|
||||
},
|
||||
jestConfigs: {
|
||||
coverageDirectory: 'coverage',
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.(t|j)s'],
|
||||
coverageReporters: ['clover', 'json', 'lcov', 'text', 'text-summary', 'cobertura'],
|
||||
},
|
||||
});
|
||||
|
||||
export default config;
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"watchAssets": true,
|
||||
"assets": [
|
||||
{
|
||||
"include": "config",
|
||||
"exclude": "**/*.md"
|
||||
},
|
||||
"i18n",
|
||||
"files"
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
-17939
File diff suppressed because it is too large
Load Diff
-119
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"name": "test",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"typeorm:cli": "./node_modules/.bin/ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm",
|
||||
"typeorm:cli-d": "ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm -d typeorm.cli.ts",
|
||||
"migration:generate": "npm run typeorm:cli-d migration:generate",
|
||||
"migration:create": "npm run typeorm:cli migration:create",
|
||||
"migration:up": "npm run typeorm:cli-d migration:run",
|
||||
"migration:down": "npm run typeorm:cli-d migration:revert"
|
||||
},
|
||||
"dependencies": {
|
||||
"@abdalhamid/hello": "^2.0.0",
|
||||
"@hamid/hello": "file:../libraries/test-package",
|
||||
"@keyv/redis": "^4.0.2",
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/axios": "^3.1.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/event-emitter": "^2.1.1",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/microservices": "^10.4.7",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.4.8",
|
||||
"@nestjs/swagger": "^8.0.5",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/throttler": "^6.2.1",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"amqp-connection-manager": "^4.1.14",
|
||||
"amqplib": "^0.10.4",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cacheable": "^1.8.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"firebase-admin": "^13.0.2",
|
||||
"google-libphonenumber": "^3.2.39",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"nestjs-i18n": "^10.4.9",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"nestjs-twilio": "^4.4.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"oci-common": "^2.99.0",
|
||||
"oci-sdk": "^2.99.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"typeorm-transactional": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@golevelup/ts-jest": "^0.6.0",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/google-libphonenumber": "^7.4.30",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"i": "^0.3.7",
|
||||
"jest": "^29.5.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"npm": "^10.9.2",
|
||||
"prettier": "^2.8.8",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts}": [
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{js,ts,json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { JuniorModule } from '~/junior/junior.module';
|
||||
import { AllowanceChangeRequestController, AllowancesController } from './controllers';
|
||||
import { Allowance, AllowanceChangeRequest } from './entities';
|
||||
import { AllowanceChangeRequestsRepository, AllowancesRepository } from './repositories';
|
||||
import { AllowanceChangeRequestsService, AllowancesService } from './services';
|
||||
|
||||
@Module({
|
||||
controllers: [AllowancesController, AllowanceChangeRequestController],
|
||||
imports: [TypeOrmModule.forFeature([Allowance, AllowanceChangeRequest]), JuniorModule],
|
||||
providers: [
|
||||
AllowancesService,
|
||||
AllowancesRepository,
|
||||
AllowanceChangeRequestsService,
|
||||
AllowanceChangeRequestsRepository,
|
||||
],
|
||||
exports: [],
|
||||
})
|
||||
export class AllowanceModule {}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||
import { AllowanceChangeRequestResponseDto } from '../dtos/response';
|
||||
import { AllowanceChangeRequestsService } from '../services';
|
||||
|
||||
@Controller('allowance-change-requests')
|
||||
@ApiTags('Allowance Change Requests')
|
||||
@ApiBearerAuth()
|
||||
export class AllowanceChangeRequestController {
|
||||
constructor(private readonly allowanceChangeRequestsService: AllowanceChangeRequestsService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.JUNIOR)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
requestAllowanceChange(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceChangeRequestDto) {
|
||||
return this.allowanceChangeRequestsService.createAllowanceChangeRequest(sub, body);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataPageResponse(AllowanceChangeRequestResponseDto)
|
||||
async findAllowanceChangeRequests(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
|
||||
const [requests, itemCount] = await this.allowanceChangeRequestsService.findAllowanceChangeRequests(sub, query);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
requests.map((request) => new AllowanceChangeRequestResponseDto(request)),
|
||||
{
|
||||
itemCount,
|
||||
page: query.page,
|
||||
size: query.size,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get('/:changeRequestId')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(AllowanceChangeRequestResponseDto)
|
||||
async findAllowanceChangeRequestById(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||
) {
|
||||
const request = await this.allowanceChangeRequestsService.findAllowanceChangeRequestById(sub, changeRequestId);
|
||||
|
||||
return ResponseFactory.data(new AllowanceChangeRequestResponseDto(request));
|
||||
}
|
||||
|
||||
@Patch(':changeRequestId/approve')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
approveAllowanceChangeRequest(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||
) {
|
||||
return this.allowanceChangeRequestsService.approveAllowanceChangeRequest(sub, changeRequestId);
|
||||
}
|
||||
|
||||
@Patch(':changeRequestId/reject')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
rejectAllowanceChangeRequest(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('changeRequestId', CustomParseUUIDPipe) changeRequestId: string,
|
||||
) {
|
||||
return this.allowanceChangeRequestsService.rejectAllowanceChangeRequest(sub, changeRequestId);
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { AllowedRoles, AuthenticatedUser } from '~/common/decorators';
|
||||
import { RolesGuard } from '~/common/guards';
|
||||
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CustomParseUUIDPipe } from '~/core/pipes';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||
import { AllowanceResponseDto } from '../dtos/response';
|
||||
import { AllowancesService } from '../services';
|
||||
|
||||
@Controller('allowances')
|
||||
@ApiTags('Allowances')
|
||||
@ApiBearerAuth()
|
||||
export class AllowancesController {
|
||||
constructor(private readonly allowancesService: AllowancesService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(AllowanceResponseDto)
|
||||
async createAllowance(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: CreateAllowanceRequestDto) {
|
||||
const allowance = await this.allowancesService.createAllowance(sub, body);
|
||||
|
||||
return ResponseFactory.data(new AllowanceResponseDto(allowance));
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataPageResponse(AllowanceResponseDto)
|
||||
async findAllowances(@AuthenticatedUser() { sub }: IJwtPayload, @Query() query: PageOptionsRequestDto) {
|
||||
const [allowances, itemCount] = await this.allowancesService.findAllowances(sub, query);
|
||||
|
||||
return ResponseFactory.dataPage(
|
||||
allowances.map((allowance) => new AllowanceResponseDto(allowance)),
|
||||
{
|
||||
itemCount,
|
||||
page: query.page,
|
||||
size: query.size,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':allowanceId')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(AllowanceResponseDto)
|
||||
async findAllowanceById(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
|
||||
) {
|
||||
const allowance = await this.allowancesService.findAllowanceById(allowanceId, sub);
|
||||
|
||||
return ResponseFactory.data(new AllowanceResponseDto(allowance));
|
||||
}
|
||||
|
||||
@Delete(':allowanceId')
|
||||
@UseGuards(RolesGuard)
|
||||
@AllowedRoles(Roles.GUARDIAN)
|
||||
@ApiDataResponse(AllowanceResponseDto)
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteAllowance(
|
||||
@AuthenticatedUser() { sub }: IJwtPayload,
|
||||
@Param('allowanceId', CustomParseUUIDPipe) allowanceId: string,
|
||||
) {
|
||||
return this.allowancesService.deleteAllowance(sub, allowanceId);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.controller';
|
||||
export * from './allowances.controller';
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
|
||||
export class CreateAllowanceChangeRequestDto {
|
||||
@ApiProperty({ example: 'I want to change the amount of the allowance' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowanceChangeRequest.reason' }) })
|
||||
@IsNotEmpty({
|
||||
message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowanceChangeRequest.reason' }),
|
||||
})
|
||||
reason!: string;
|
||||
|
||||
@ApiProperty({ example: 100 })
|
||||
@IsNumber(
|
||||
{},
|
||||
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowanceChangeRequest.amount' }) },
|
||||
)
|
||||
@IsPositive({
|
||||
message: i18n('validation.IsPositive', { path: 'general', property: 'allowanceChangeRequest.amount' }),
|
||||
})
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||
@IsUUID('4', {
|
||||
message: i18n('validation.IsUUID', { path: 'general', property: 'allowanceChangeRequest.allowanceId' }),
|
||||
})
|
||||
allowanceId!: string;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsDate, IsEnum, IsInt, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID, ValidateIf } from 'class-validator';
|
||||
import moment from 'moment';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
|
||||
export class CreateAllowanceRequestDto {
|
||||
@ApiProperty({ example: 'Allowance name' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'allowance.name' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'allowance.name' }) })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: 100 })
|
||||
@IsNumber({}, { message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.amount' }) })
|
||||
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
|
||||
@IsEnum(AllowanceFrequency, {
|
||||
message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.frequency' }),
|
||||
})
|
||||
frequency!: AllowanceFrequency;
|
||||
|
||||
@ApiProperty({ example: AllowanceType.BY_END_DATE })
|
||||
@IsEnum(AllowanceType, { message: i18n('validation.IsEnum', { path: 'general', property: 'allowance.type' }) })
|
||||
type!: AllowanceType;
|
||||
|
||||
@ApiProperty({ example: new Date() })
|
||||
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.startDate' }) })
|
||||
@Transform(({ value }) => moment(value).startOf('day').toDate())
|
||||
startDate!: Date;
|
||||
|
||||
@ApiProperty({ example: new Date() })
|
||||
@IsDate({ message: i18n('validation.IsDate', { path: 'general', property: 'allowance.endDate' }) })
|
||||
@Transform(({ value }) => moment(value).endOf('day').toDate())
|
||||
@ValidateIf((o) => o.type === AllowanceType.BY_END_DATE)
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty({ example: 10 })
|
||||
@IsNumber(
|
||||
{},
|
||||
{ message: i18n('validation.IsNumber', { path: 'general', property: 'allowance.numberOfTransactions' }) },
|
||||
)
|
||||
@IsInt({ message: i18n('validation.IsInt', { path: 'general', property: 'allowance.amount' }) })
|
||||
@IsPositive({ message: i18n('validation.IsPositive', { path: 'general', property: 'allowance.amount' }) })
|
||||
@ValidateIf((o) => o.type === AllowanceType.BY_COUNT)
|
||||
numberOfTransactions?: number;
|
||||
|
||||
@ApiProperty({ example: 'e7b1b3b4-4b3b-4b3b-4b3b-4b3b4b3b4b3b' })
|
||||
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'allowance.juniorId' }) })
|
||||
juniorId!: string;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './create-allowance-change.request.dto';
|
||||
export * from './create-allowance.request.dto';
|
||||
@@ -1,45 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { AllowanceChangeRequest } from '~/allowance/entities';
|
||||
import { AllowanceChangeRequestStatus } from '~/allowance/enums';
|
||||
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||
|
||||
export class AllowanceChangeRequestResponseDto {
|
||||
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: AllowanceChangeRequestStatus.APPROVED })
|
||||
status!: AllowanceChangeRequestStatus;
|
||||
|
||||
@ApiProperty({ example: 'Allowance name' })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: '100' })
|
||||
oldAmount!: number;
|
||||
|
||||
@ApiProperty({ example: '200' })
|
||||
newAmount!: number;
|
||||
|
||||
@ApiProperty({ example: 'Some reason' })
|
||||
reason!: string;
|
||||
|
||||
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||
allowanceId!: string;
|
||||
|
||||
@ApiProperty({ type: JuniorResponseDto })
|
||||
junior!: JuniorResponseDto;
|
||||
|
||||
@ApiProperty({ example: new Date() })
|
||||
createdAt!: Date;
|
||||
|
||||
constructor(allowanceChangeRequest: AllowanceChangeRequest) {
|
||||
this.id = allowanceChangeRequest.id;
|
||||
this.status = allowanceChangeRequest.status;
|
||||
this.name = allowanceChangeRequest.allowance.name;
|
||||
this.oldAmount = allowanceChangeRequest.allowance.amount;
|
||||
this.newAmount = allowanceChangeRequest.amount;
|
||||
this.reason = allowanceChangeRequest.reason;
|
||||
this.allowanceId = allowanceChangeRequest.allowanceId;
|
||||
this.junior = new JuniorResponseDto(allowanceChangeRequest.allowance.junior);
|
||||
this.createdAt = allowanceChangeRequest.createdAt;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Allowance } from '~/allowance/entities';
|
||||
import { AllowanceFrequency, AllowanceType } from '~/allowance/enums';
|
||||
import { JuniorResponseDto } from '~/junior/dtos/response';
|
||||
|
||||
export class AllowanceResponseDto {
|
||||
@ApiProperty({ example: 'd641bb71-2e7c-4e62-96fa-2785f0a651c6' })
|
||||
id!: string;
|
||||
|
||||
@ApiProperty({ example: 'Allowance name' })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ example: 100 })
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ example: AllowanceFrequency.WEEKLY })
|
||||
frequency!: AllowanceFrequency;
|
||||
|
||||
@ApiProperty({ example: AllowanceType.BY_END_DATE })
|
||||
type!: AllowanceType;
|
||||
|
||||
@ApiProperty({ example: new Date() })
|
||||
startDate!: Date;
|
||||
|
||||
@ApiProperty({ example: new Date() })
|
||||
endDate?: Date;
|
||||
|
||||
@ApiProperty({ example: 10 })
|
||||
numberOfTransactions?: number;
|
||||
|
||||
@ApiProperty({ type: JuniorResponseDto })
|
||||
junior!: JuniorResponseDto;
|
||||
|
||||
@ApiProperty({ example: new Date() })
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty({ example: new Date() })
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(allowance: Allowance) {
|
||||
this.id = allowance.id;
|
||||
this.name = allowance.name;
|
||||
this.amount = allowance.amount;
|
||||
this.frequency = allowance.frequency;
|
||||
this.type = allowance.type;
|
||||
this.startDate = allowance.startDate;
|
||||
this.endDate = allowance.endDate;
|
||||
this.numberOfTransactions = allowance.numberOfTransactions;
|
||||
this.junior = new JuniorResponseDto(allowance.junior);
|
||||
this.createdAt = allowance.createdAt;
|
||||
this.updatedAt = allowance.updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.response.dto';
|
||||
export * from './allowance.response.dto';
|
||||
@@ -1,45 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { AllowanceChangeRequestStatus } from '../enums';
|
||||
import { Allowance } from './allowance.entity';
|
||||
|
||||
@Entity('allowance_change_requests')
|
||||
export class AllowanceChangeRequest {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'text', name: 'reason' })
|
||||
reason!: string;
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
name: 'amount',
|
||||
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||
})
|
||||
amount!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'status', default: AllowanceChangeRequestStatus.PENDING })
|
||||
status!: AllowanceChangeRequestStatus;
|
||||
|
||||
@Column({ type: 'uuid', name: 'allowance_id' })
|
||||
allowanceId!: string;
|
||||
|
||||
@ManyToOne(() => Allowance, (allowance) => allowance.changeRequests)
|
||||
@JoinColumn({ name: 'allowance_id' })
|
||||
allowance!: Allowance;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { Junior } from '~/junior/entities';
|
||||
import { AllowanceFrequency, AllowanceType } from '../enums';
|
||||
import { AllowanceChangeRequest } from './allowance-change-request.entity';
|
||||
|
||||
@Entity('allowances')
|
||||
export class Allowance {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'name' })
|
||||
name!: string;
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
name: 'amount',
|
||||
transformer: { to: (value: number) => value, from: (value: string) => parseFloat(value) },
|
||||
})
|
||||
amount!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'frequency' })
|
||||
frequency!: AllowanceFrequency;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'type' })
|
||||
type!: AllowanceType;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'start_date' })
|
||||
startDate!: Date;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', name: 'end_date', nullable: true })
|
||||
endDate?: Date;
|
||||
|
||||
@Column({ type: 'int', name: 'number_of_transactions', nullable: true })
|
||||
numberOfTransactions?: number;
|
||||
|
||||
@Column({ type: 'uuid', name: 'guardian_id' })
|
||||
guardianId!: string;
|
||||
|
||||
@Column({ type: 'uuid', name: 'junior_id' })
|
||||
juniorId!: string;
|
||||
|
||||
@ManyToOne(() => Guardian, (guardian) => guardian.allowances)
|
||||
@JoinColumn({ name: 'guardian_id' })
|
||||
guardian!: Guardian;
|
||||
|
||||
@ManyToOne(() => Junior, (junior) => junior.allowances)
|
||||
@JoinColumn({ name: 'junior_id' })
|
||||
junior!: Junior;
|
||||
|
||||
@OneToMany(() => AllowanceChangeRequest, (changeRequest) => changeRequest.allowance)
|
||||
changeRequests!: AllowanceChangeRequest[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamp with time zone', nullable: true })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.entity';
|
||||
export * from './allowance.entity';
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum AllowanceChangeRequestStatus {
|
||||
PENDING = 'PENDING',
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum AllowanceFrequency {
|
||||
DAILY = 'DAILY',
|
||||
WEEKLY = 'WEEKLY',
|
||||
MONTHLY = 'MONTHLY',
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum AllowanceType {
|
||||
BY_END_DATE = 'BY_END_DATE',
|
||||
BY_COUNT = 'BY_COUNT',
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './allowance-change-request-status.enum';
|
||||
export * from './allowance-frequency.enum';
|
||||
export * from './allowance-type.enum';
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||
import { AllowanceChangeRequest } from '../entities';
|
||||
import { AllowanceChangeRequestStatus } from '../enums';
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class AllowanceChangeRequestsRepository {
|
||||
constructor(
|
||||
@InjectRepository(AllowanceChangeRequest)
|
||||
private readonly allowanceChangeRequestsRepository: Repository<AllowanceChangeRequest>,
|
||||
) {}
|
||||
|
||||
createAllowanceChangeRequest(allowanceId: string, body: CreateAllowanceChangeRequestDto) {
|
||||
return this.allowanceChangeRequestsRepository.save(
|
||||
this.allowanceChangeRequestsRepository.create({
|
||||
allowanceId,
|
||||
amount: body.amount,
|
||||
reason: body.reason,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>, withRelations = false) {
|
||||
const relations = withRelations
|
||||
? ['allowance', 'allowance.junior', 'allowance.junior.customer', 'allowance.junior.customer.profilePicture']
|
||||
: [];
|
||||
return this.allowanceChangeRequestsRepository.findOne({ where, relations });
|
||||
}
|
||||
|
||||
updateAllowanceChangeRequestStatus(requestId: string, status: AllowanceChangeRequestStatus) {
|
||||
return this.allowanceChangeRequestsRepository.update({ id: requestId }, { status });
|
||||
}
|
||||
|
||||
findAllowanceChangeRequests(guardianId: string, query: PageOptionsRequestDto) {
|
||||
return this.allowanceChangeRequestsRepository.findAndCount({
|
||||
where: { allowance: { guardianId } },
|
||||
take: query.size,
|
||||
skip: query.size * (query.page - ONE),
|
||||
relations: [
|
||||
'allowance',
|
||||
'allowance.junior',
|
||||
'allowance.junior.customer',
|
||||
'allowance.junior.customer.profilePicture',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||
import { Allowance } from '../entities';
|
||||
const ONE = 1;
|
||||
@Injectable()
|
||||
export class AllowancesRepository {
|
||||
constructor(@InjectRepository(Allowance) private readonly allowancesRepository: Repository<Allowance>) {}
|
||||
|
||||
createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
|
||||
return this.allowancesRepository.save(
|
||||
this.allowancesRepository.create({
|
||||
guardianId,
|
||||
name: body.name,
|
||||
amount: body.amount,
|
||||
frequency: body.frequency,
|
||||
type: body.type,
|
||||
startDate: body.startDate,
|
||||
endDate: body.endDate,
|
||||
numberOfTransactions: body.numberOfTransactions,
|
||||
juniorId: body.juniorId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||
return this.allowancesRepository.findOne({
|
||||
where: { id: allowanceId, guardianId },
|
||||
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||
});
|
||||
}
|
||||
|
||||
findAllowances(guardianId: string, query: PageOptionsRequestDto) {
|
||||
return this.allowancesRepository.findAndCount({
|
||||
where: { guardianId },
|
||||
relations: ['junior', 'junior.customer', 'junior.customer.profilePicture'],
|
||||
take: query.size,
|
||||
skip: query.size * (query.page - ONE),
|
||||
});
|
||||
}
|
||||
|
||||
deleteAllowance(guardianId: string, allowanceId: string) {
|
||||
return this.allowancesRepository.softDelete({ id: allowanceId, guardianId });
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-request.repository';
|
||||
export * from './allowances.repository';
|
||||
@@ -1,115 +0,0 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { OciService } from '~/document/services';
|
||||
import { CreateAllowanceChangeRequestDto } from '../dtos/request';
|
||||
import { AllowanceChangeRequest } from '../entities';
|
||||
import { AllowanceChangeRequestStatus } from '../enums';
|
||||
import { AllowanceChangeRequestsRepository } from '../repositories';
|
||||
import { AllowancesService } from './allowances.service';
|
||||
|
||||
@Injectable()
|
||||
export class AllowanceChangeRequestsService {
|
||||
constructor(
|
||||
private readonly allowanceChangeRequestsRepository: AllowanceChangeRequestsRepository,
|
||||
private readonly ociService: OciService,
|
||||
private readonly allowanceService: AllowancesService,
|
||||
) {}
|
||||
|
||||
async createAllowanceChangeRequest(juniorId: string, body: CreateAllowanceChangeRequestDto) {
|
||||
const allowance = await this.allowanceService.validateAllowanceForJunior(juniorId, body.allowanceId);
|
||||
|
||||
if (allowance.amount === body.amount) {
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT');
|
||||
}
|
||||
|
||||
const requestWithTheSameAmount = await this.findAllowanceChangeRequestBy({
|
||||
allowanceId: body.allowanceId,
|
||||
amount: body.amount,
|
||||
status: AllowanceChangeRequestStatus.PENDING,
|
||||
});
|
||||
|
||||
if (requestWithTheSameAmount) {
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT_PENDING');
|
||||
}
|
||||
|
||||
return this.allowanceChangeRequestsRepository.createAllowanceChangeRequest(body.allowanceId, body);
|
||||
}
|
||||
|
||||
findAllowanceChangeRequestBy(where: FindOptionsWhere<AllowanceChangeRequest>) {
|
||||
return this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(where);
|
||||
}
|
||||
|
||||
async approveAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||
|
||||
if (!request) {
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||
}
|
||||
if (request.status === AllowanceChangeRequestStatus.APPROVED) {
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_APPROVED');
|
||||
}
|
||||
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
|
||||
requestId,
|
||||
AllowanceChangeRequestStatus.APPROVED,
|
||||
);
|
||||
}
|
||||
|
||||
async rejectAllowanceChangeRequest(guardianId: string, requestId: string) {
|
||||
const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } });
|
||||
|
||||
if (!request) {
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||
}
|
||||
if (request.status === AllowanceChangeRequestStatus.REJECTED) {
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_REJECTED');
|
||||
}
|
||||
return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus(
|
||||
requestId,
|
||||
AllowanceChangeRequestStatus.REJECTED,
|
||||
);
|
||||
}
|
||||
|
||||
async findAllowanceChangeRequests(
|
||||
guardianId: string,
|
||||
query: PageOptionsRequestDto,
|
||||
): Promise<[AllowanceChangeRequest[], number]> {
|
||||
const [requests, itemCount] = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequests(
|
||||
guardianId,
|
||||
query,
|
||||
);
|
||||
|
||||
await this.prepareAllowanceChangeRequestsImages(requests);
|
||||
|
||||
return [requests, itemCount];
|
||||
}
|
||||
|
||||
async findAllowanceChangeRequestById(guardianId: string, requestId: string) {
|
||||
const request = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(
|
||||
{
|
||||
id: requestId,
|
||||
allowance: { guardianId },
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (!request) {
|
||||
throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND');
|
||||
}
|
||||
|
||||
await this.prepareAllowanceChangeRequestsImages([request]);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private prepareAllowanceChangeRequestsImages(requests: AllowanceChangeRequest[]) {
|
||||
return Promise.all(
|
||||
requests.map(async (request) => {
|
||||
const profilePicture = request.allowance.junior.customer.profilePicture;
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import moment from 'moment';
|
||||
import { PageOptionsRequestDto } from '~/core/dtos';
|
||||
import { OciService } from '~/document/services';
|
||||
import { JuniorService } from '~/junior/services';
|
||||
import { CreateAllowanceRequestDto } from '../dtos/request';
|
||||
import { Allowance } from '../entities';
|
||||
import { AllowancesRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class AllowancesService {
|
||||
constructor(
|
||||
private readonly allowancesRepository: AllowancesRepository,
|
||||
private readonly juniorService: JuniorService,
|
||||
private readonly ociService: OciService,
|
||||
) {}
|
||||
|
||||
async createAllowance(guardianId: string, body: CreateAllowanceRequestDto) {
|
||||
if (moment(body.startDate).isBefore(moment().startOf('day'))) {
|
||||
throw new BadRequestException('ALLOWANCE.START_DATE_BEFORE_TODAY');
|
||||
}
|
||||
if (moment(body.startDate).isAfter(body.endDate)) {
|
||||
throw new BadRequestException('ALLOWANCE.START_DATE_AFTER_END_DATE');
|
||||
}
|
||||
|
||||
const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(guardianId, body.juniorId);
|
||||
|
||||
if (!doesJuniorBelongToGuardian) {
|
||||
throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN');
|
||||
}
|
||||
|
||||
const allowance = await this.allowancesRepository.createAllowance(guardianId, body);
|
||||
|
||||
return this.findAllowanceById(allowance.id);
|
||||
}
|
||||
|
||||
async findAllowanceById(allowanceId: string, guardianId?: string) {
|
||||
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId, guardianId);
|
||||
|
||||
if (!allowance) {
|
||||
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||
}
|
||||
await this.prepareAllowanceDocuments([allowance]);
|
||||
return allowance;
|
||||
}
|
||||
|
||||
async findAllowances(guardianId: string, query: PageOptionsRequestDto): Promise<[Allowance[], number]> {
|
||||
const [allowances, itemCount] = await this.allowancesRepository.findAllowances(guardianId, query);
|
||||
await this.prepareAllowanceDocuments(allowances);
|
||||
return [allowances, itemCount];
|
||||
}
|
||||
|
||||
async deleteAllowance(guardianId: string, allowanceId: string) {
|
||||
const { affected } = await this.allowancesRepository.deleteAllowance(guardianId, allowanceId);
|
||||
|
||||
if (!affected) {
|
||||
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||
}
|
||||
}
|
||||
|
||||
async validateAllowanceForJunior(juniorId: string, allowanceId: string) {
|
||||
const allowance = await this.allowancesRepository.findAllowanceById(allowanceId);
|
||||
|
||||
if (!allowance) {
|
||||
throw new BadRequestException('ALLOWANCE.NOT_FOUND');
|
||||
}
|
||||
|
||||
if (allowance.juniorId !== juniorId) {
|
||||
throw new BadRequestException('ALLOWANCE.DOES_NOT_BELONG_TO_JUNIOR');
|
||||
}
|
||||
|
||||
return allowance;
|
||||
}
|
||||
|
||||
private async prepareAllowanceDocuments(allowance: Allowance[]) {
|
||||
await Promise.all(
|
||||
allowance.map(async (allowance) => {
|
||||
const profilePicture = allowance.junior.customer.profilePicture;
|
||||
if (profilePicture) {
|
||||
profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './allowance-change-requests.service';
|
||||
export * from './allowances.service';
|
||||
@@ -1,95 +0,0 @@
|
||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { addTransactionalDataSource } from 'typeorm-transactional';
|
||||
import { AllowanceModule } from './allowance/allowance.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { CacheModule } from './common/modules/cache/cache.module';
|
||||
import { LookupModule } from './common/modules/lookup/lookup.module';
|
||||
import { NotificationModule } from './common/modules/notification/notification.module';
|
||||
import { OtpModule } from './common/modules/otp/otp.module';
|
||||
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
|
||||
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
|
||||
import { buildI18nOptions } from './core/module-options/i18n-options';
|
||||
import { buildValidationPipe } from './core/pipes';
|
||||
import { CustomerModule } from './customer/customer.module';
|
||||
import { migrations } from './db';
|
||||
import { DocumentModule } from './document/document.module';
|
||||
import { GiftModule } from './gift/gift.module';
|
||||
import { GuardianModule } from './guardian/guardian.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { JuniorModule } from './junior/junior.module';
|
||||
import { MoneyRequestModule } from './money-request/money-request.module';
|
||||
import { SavingGoalsModule } from './saving-goals/saving-goals.module';
|
||||
import { TaskModule } from './task/task.module';
|
||||
|
||||
@Module({
|
||||
controllers: [],
|
||||
imports: [
|
||||
ConfigModule.forRoot(buildConfigOptions()),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => {
|
||||
return buildTypeormOptions(config, migrations);
|
||||
},
|
||||
/* eslint-disable require-await */
|
||||
async dataSourceFactory(options) {
|
||||
if (!options) {
|
||||
throw new Error('Invalid options passed');
|
||||
}
|
||||
|
||||
return addTransactionalDataSource(new DataSource(options));
|
||||
},
|
||||
/* eslint-enable require-await */
|
||||
}),
|
||||
LoggerModule.forRootAsync({
|
||||
useFactory: (config: ConfigService) => buildLoggerOptions(config),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
I18nModule.forRoot(buildI18nOptions()),
|
||||
|
||||
CacheModule,
|
||||
|
||||
// App modules
|
||||
AuthModule,
|
||||
CustomerModule,
|
||||
JuniorModule,
|
||||
|
||||
TaskModule,
|
||||
GuardianModule,
|
||||
SavingGoalsModule,
|
||||
AllowanceModule,
|
||||
MoneyRequestModule,
|
||||
GiftModule,
|
||||
|
||||
OtpModule,
|
||||
DocumentModule,
|
||||
LookupModule,
|
||||
|
||||
HealthModule,
|
||||
|
||||
NotificationModule,
|
||||
],
|
||||
providers: [
|
||||
// Global Pipes
|
||||
{
|
||||
inject: [ConfigService],
|
||||
provide: APP_PIPE,
|
||||
useFactory: (config: ConfigService) => buildValidationPipe(config),
|
||||
},
|
||||
|
||||
// Global Filters (order matters)
|
||||
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
|
||||
{ provide: APP_FILTER, useValue: buildI18nValidationExceptionFilter() },
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(I18nMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { JuniorModule } from '~/junior/junior.module';
|
||||
import { AuthController } from './controllers';
|
||||
import { Device, User } from './entities';
|
||||
import { DeviceRepository, UserRepository } from './repositories';
|
||||
import { AuthService, DeviceService } from './services';
|
||||
import { UserService } from './services/user.service';
|
||||
import { AccessTokenStrategy } from './strategies';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([User, Device]),
|
||||
JwtModule.register({}),
|
||||
forwardRef(() => CustomerModule),
|
||||
forwardRef(() => JuniorModule),
|
||||
],
|
||||
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [UserService, DeviceService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -1 +0,0 @@
|
||||
export const COUNTRY_CODE_REGEX = /^\+\d{1,3}$/;
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './country-code-regex.constant.';
|
||||
export * from './passcode-regext.constant';
|
||||
export * from './password-regex.constant';
|
||||
@@ -1 +0,0 @@
|
||||
export const PASSCODE_REGEX = /^\d{6}$/;
|
||||
@@ -1 +0,0 @@
|
||||
export const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&#\-_])[A-Za-z\d@$!%*?&#\-_]{8,}$/;
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { DEVICE_ID_HEADER } from '~/common/constants';
|
||||
import { AuthenticatedUser, Public } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import {
|
||||
CreateUnverifiedUserRequestDto,
|
||||
DisableBiometricRequestDto,
|
||||
EnableBiometricRequestDto,
|
||||
ForgetPasswordRequestDto,
|
||||
LoginRequestDto,
|
||||
RefreshTokenRequestDto,
|
||||
SendForgetPasswordOtpRequestDto,
|
||||
SetEmailRequestDto,
|
||||
setJuniorPasswordRequestDto,
|
||||
SetPasscodeRequestDto,
|
||||
VerifyUserRequestDto,
|
||||
} from '../dtos/request';
|
||||
import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response';
|
||||
import { LoginResponseDto } from '../dtos/response/login.response.dto';
|
||||
import { IJwtPayload } from '../interfaces';
|
||||
import { AuthService } from '../services';
|
||||
|
||||
@Controller('auth')
|
||||
@ApiTags('Auth')
|
||||
@ApiBearerAuth()
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
@Post('register/otp')
|
||||
async register(@Body() createUnverifiedUserDto: CreateUnverifiedUserRequestDto) {
|
||||
const phoneNumber = await this.authService.sendRegisterOtp(createUnverifiedUserDto);
|
||||
return ResponseFactory.data(new SendRegisterOtpResponseDto(phoneNumber));
|
||||
}
|
||||
|
||||
@Post('register/verify')
|
||||
async verifyUser(@Body() verifyUserDto: VerifyUserRequestDto) {
|
||||
const [res, user] = await this.authService.verifyUser(verifyUserDto);
|
||||
return ResponseFactory.data(new LoginResponseDto(res, user));
|
||||
}
|
||||
|
||||
@Post('register/set-email')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async setEmail(@AuthenticatedUser() { sub }: IJwtPayload, @Body() setEmailDto: SetEmailRequestDto) {
|
||||
await this.authService.setEmail(sub, setEmailDto);
|
||||
}
|
||||
|
||||
@Post('register/set-passcode')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async setPasscode(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { passcode }: SetPasscodeRequestDto) {
|
||||
await this.authService.setPasscode(sub, passcode);
|
||||
}
|
||||
|
||||
@Post('biometric/enable')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
enableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() enableBiometricDto: EnableBiometricRequestDto) {
|
||||
return this.authService.enableBiometric(sub, enableBiometricDto);
|
||||
}
|
||||
|
||||
@Post('biometric/disable')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
disableBiometric(@AuthenticatedUser() { sub }: IJwtPayload, @Body() disableBiometricDto: DisableBiometricRequestDto) {
|
||||
return this.authService.disableBiometric(sub, disableBiometricDto);
|
||||
}
|
||||
|
||||
@Post('forget-password/otp')
|
||||
async forgetPassword(@Body() sendForgetPasswordOtpDto: SendForgetPasswordOtpRequestDto) {
|
||||
const email = await this.authService.sendForgetPasswordOtp(sendForgetPasswordOtpDto);
|
||||
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(email));
|
||||
}
|
||||
|
||||
@Post('forget-password/reset')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
resetPassword(@Body() forgetPasswordDto: ForgetPasswordRequestDto) {
|
||||
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
|
||||
}
|
||||
|
||||
@Post('junior/set-passcode')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Public()
|
||||
setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
|
||||
return this.authService.setJuniorPasscode(setPasscodeDto);
|
||||
}
|
||||
|
||||
@Post('refresh-token')
|
||||
@Public()
|
||||
async refreshToken(@Body() { refreshToken }: RefreshTokenRequestDto) {
|
||||
const [res, user] = await this.authService.refreshToken(refreshToken);
|
||||
return ResponseFactory.data(new LoginResponseDto(res, user));
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
|
||||
const [res, user] = await this.authService.login(loginDto, deviceId);
|
||||
return ResponseFactory.data(new LoginResponseDto(res, user));
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async logout(@Req() request: Request) {
|
||||
await this.authService.logout(request);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './auth.controller';
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Matches } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
|
||||
import { IsValidPhoneNumber } from '~/core/decorators/validations';
|
||||
|
||||
export class CreateUnverifiedUserRequestDto {
|
||||
@ApiProperty({ example: '+962' })
|
||||
@Matches(COUNTRY_CODE_REGEX, {
|
||||
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
|
||||
})
|
||||
countryCode: string = '+966';
|
||||
|
||||
@ApiProperty({ example: '787259134' })
|
||||
@IsValidPhoneNumber({
|
||||
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
|
||||
})
|
||||
phoneNumber!: string;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PickType } from '@nestjs/swagger';
|
||||
import { EnableBiometricRequestDto } from './enable-biometric.request.dto';
|
||||
|
||||
export class DisableBiometricRequestDto extends PickType(EnableBiometricRequestDto, ['deviceId']) {}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class EnableBiometricRequestDto {
|
||||
@ApiProperty({ example: 'device-id' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.deviceId' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.deviceId' }) })
|
||||
deviceId!: string;
|
||||
|
||||
@ApiProperty({ example: 'publicKey' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.publicKey' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.publicKey' }) })
|
||||
publicKey!: string;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsNotEmpty, IsNumberString, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
|
||||
export class ForgetPasswordRequestDto {
|
||||
@ApiProperty({ example: '[email protected]' })
|
||||
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: 'password' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.password' }) })
|
||||
password!: string;
|
||||
|
||||
@ApiProperty({ example: 'password' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.confirmPassword' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.confirmPassword' }) })
|
||||
confirmPassword!: string;
|
||||
|
||||
@ApiProperty({ example: '111111' })
|
||||
@IsNumberString(
|
||||
{ no_symbols: true },
|
||||
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
|
||||
)
|
||||
@MaxLength(DEFAULT_OTP_LENGTH, {
|
||||
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
|
||||
})
|
||||
@MinLength(DEFAULT_OTP_LENGTH, {
|
||||
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
|
||||
})
|
||||
otp!: string;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export * from './create-unverified-user.request.dto';
|
||||
export * from './disable-biometric.request.dto';
|
||||
export * from './enable-biometric.request.dto';
|
||||
export * from './forget-password.request.dto';
|
||||
export * from './login.request.dto';
|
||||
export * from './refresh-token.request.dto';
|
||||
export * from './send-forget-password-otp.request.dto';
|
||||
export * from './set-email.request.dto';
|
||||
export * from './set-junior-password.request.dto';
|
||||
export * from './set-passcode.request.dto';
|
||||
export * from './verify-user.request.dto';
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { GrantType } from '~/auth/enums';
|
||||
export class LoginRequestDto {
|
||||
@ApiProperty({ example: GrantType.PASSWORD })
|
||||
@IsEnum(GrantType, { message: i18n('validation.IsEnum', { path: 'general', property: 'auth.grantType' }) })
|
||||
grantType!: GrantType;
|
||||
|
||||
@ApiProperty({ example: '[email protected]' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.email' }) })
|
||||
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: '123456' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
|
||||
@ValidateIf((o) => o.grantType === GrantType.PASSWORD)
|
||||
password!: string;
|
||||
|
||||
@ApiProperty({ example: 'fcm-device-token' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.fcmToken' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.fcmToken' }) })
|
||||
@IsOptional()
|
||||
fcmToken?: string;
|
||||
|
||||
@ApiProperty({ example: 'Login signature' })
|
||||
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.signature' }) })
|
||||
@ValidateIf((o) => o.grantType === GrantType.BIOMETRIC)
|
||||
signature!: string;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class RefreshTokenRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.isString', { path: 'general', property: 'auth.refreshToken' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.required', { path: 'general', property: 'auth.refreshToken' }) })
|
||||
refreshToken!: string;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PickType } from '@nestjs/swagger';
|
||||
import { LoginRequestDto } from './login.request.dto';
|
||||
|
||||
export class SendForgetPasswordOtpRequestDto extends PickType(LoginRequestDto, ['email']) {}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
export class SetEmailRequestDto {
|
||||
@ApiProperty({ example: '[email protected]' })
|
||||
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
|
||||
email!: string;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { SetPasscodeRequestDto } from './set-passcode.request.dto';
|
||||
export class setJuniorPasswordRequestDto extends SetPasscodeRequestDto {
|
||||
@ApiProperty()
|
||||
@IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.qrToken' }) })
|
||||
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.qrToken' }) })
|
||||
qrToken!: string;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
const PASSCODE_LENGTH = 6;
|
||||
|
||||
export class SetPasscodeRequestDto {
|
||||
@ApiProperty({ example: '123456' })
|
||||
@IsNumberString(
|
||||
{ no_symbols: true },
|
||||
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.passcode' }) },
|
||||
)
|
||||
@MinLength(PASSCODE_LENGTH, { message: i18n('validation.MinLength', { path: 'general', property: 'auth.passcode' }) })
|
||||
@MaxLength(PASSCODE_LENGTH, { message: i18n('validation.MaxLength', { path: 'general', property: 'auth.passcode' }) })
|
||||
passcode!: string;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumberString, MaxLength, MinLength } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
|
||||
import { CreateUnverifiedUserRequestDto } from './create-unverified-user.request.dto';
|
||||
|
||||
export class VerifyUserRequestDto extends CreateUnverifiedUserRequestDto {
|
||||
@ApiProperty({ example: '111111' })
|
||||
@IsNumberString(
|
||||
{ no_symbols: true },
|
||||
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
|
||||
)
|
||||
@MaxLength(DEFAULT_OTP_LENGTH, {
|
||||
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
|
||||
})
|
||||
@MinLength(DEFAULT_OTP_LENGTH, {
|
||||
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
|
||||
})
|
||||
otp!: string;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './send-forget-password.response.dto';
|
||||
export * from './send-register-otp.response.dto';
|
||||
export * from './user.response.dto';
|
||||
export * from './verify-user.response.dto';
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { User } from '~/auth/entities';
|
||||
import { ILoginResponse } from '~/auth/interfaces';
|
||||
import { CustomerResponseDto } from '~/customer/dtos/response';
|
||||
import { UserResponseDto } from './user.response.dto';
|
||||
|
||||
export class LoginResponseDto {
|
||||
@ApiProperty()
|
||||
accessToken!: string;
|
||||
|
||||
@ApiProperty()
|
||||
refreshToken!: string;
|
||||
|
||||
@ApiProperty()
|
||||
expiresAt!: Date;
|
||||
|
||||
@ApiProperty({ example: UserResponseDto })
|
||||
user!: UserResponseDto;
|
||||
|
||||
@ApiProperty({ example: CustomerResponseDto })
|
||||
customer!: CustomerResponseDto;
|
||||
|
||||
constructor(IVerifyUserResponse: ILoginResponse, user: User) {
|
||||
this.user = new UserResponseDto(user);
|
||||
this.customer = new CustomerResponseDto(user.customer);
|
||||
this.accessToken = IVerifyUserResponse.accessToken;
|
||||
this.refreshToken = IVerifyUserResponse.refreshToken;
|
||||
this.expiresAt = IVerifyUserResponse.expiresAt;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export class SendForgetPasswordOtpResponseDto {
|
||||
email!: string;
|
||||
|
||||
constructor(email: string) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class SendRegisterOtpResponseDto {
|
||||
@ApiProperty()
|
||||
phoneNumber!: string;
|
||||
|
||||
constructor(phoneNumber: string) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { User } from '~/auth/entities';
|
||||
import { Roles } from '~/auth/enums';
|
||||
|
||||
export class UserResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@ApiProperty()
|
||||
phoneNumber!: string;
|
||||
|
||||
@ApiProperty()
|
||||
countryCode!: string;
|
||||
|
||||
@ApiProperty()
|
||||
isPasswordSet!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
isProfileCompleted!: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
roles!: Roles[];
|
||||
|
||||
constructor(user: User) {
|
||||
this.id = user.id;
|
||||
this.email = user.email;
|
||||
this.phoneNumber = user.phoneNumber;
|
||||
this.countryCode = user.countryCode;
|
||||
this.isPasswordSet = user.isPasswordSet;
|
||||
this.isProfileCompleted = user.isProfileCompleted;
|
||||
this.roles = user.roles;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { User } from '~/auth/entities';
|
||||
import { ILoginResponse } from '~/auth/interfaces';
|
||||
|
||||
export class VerifyUserResponseDto {
|
||||
@ApiProperty()
|
||||
accessToken!: string;
|
||||
|
||||
@ApiProperty()
|
||||
refreshToken!: string;
|
||||
|
||||
@ApiProperty()
|
||||
expiresAt!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
user!: User;
|
||||
|
||||
constructor(data: ILoginResponse, user: User) {
|
||||
this.accessToken = data.accessToken;
|
||||
this.refreshToken = data.refreshToken;
|
||||
this.expiresAt = data.expiresAt;
|
||||
this.user = user;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
|
||||
@Entity('devices')
|
||||
export class Device {
|
||||
@PrimaryColumn('varchar', { length: 255 })
|
||||
deviceId!: string;
|
||||
|
||||
@Column('varchar', { name: 'user_id' })
|
||||
userId!: string;
|
||||
|
||||
@Column('varchar', { name: 'device_name', nullable: true })
|
||||
deviceName?: string | null;
|
||||
|
||||
@Column('varchar', { name: 'public_key', nullable: true })
|
||||
publicKey?: string | null;
|
||||
|
||||
@Column('varchar', { name: 'fcm_token', nullable: true })
|
||||
fcmToken?: string | null;
|
||||
|
||||
@Column('timestamp with time zone', { name: 'last_access_on', default: () => 'CURRENT_TIMESTAMP' })
|
||||
lastAccessOn!: Date;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.devices, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user!: User;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './device.entity';
|
||||
export * from './user.entity';
|
||||
@@ -1,70 +0,0 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Notification } from '~/common/modules/notification/entities';
|
||||
import { Otp } from '~/common/modules/otp/entities';
|
||||
import { Customer } from '~/customer/entities/customer.entity';
|
||||
import { Roles } from '../enums';
|
||||
import { Device } from './device.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true, name: 'email' })
|
||||
email!: string;
|
||||
|
||||
@Column('varchar', { length: 255, name: 'phone_number' })
|
||||
phoneNumber!: string;
|
||||
|
||||
@Column('varchar', { length: 10, name: 'country_code' })
|
||||
countryCode!: string;
|
||||
|
||||
@Column('varchar', { length: 255, name: 'password', nullable: true })
|
||||
password!: string;
|
||||
|
||||
@Column('varchar', { length: 255, name: 'salt', nullable: true })
|
||||
salt!: string;
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true, name: 'google_id' })
|
||||
googleId!: string;
|
||||
|
||||
@Column('varchar', { length: 255, nullable: true, name: 'apple_id' })
|
||||
appleId!: string;
|
||||
|
||||
@Column('boolean', { default: false, name: 'is_profile_completed' })
|
||||
isProfileCompleted!: boolean;
|
||||
|
||||
@Column('text', { nullable: true, array: true, name: 'roles' })
|
||||
roles!: Roles[];
|
||||
|
||||
@OneToMany(() => Otp, (otp) => otp.user)
|
||||
otp!: Otp[];
|
||||
|
||||
@OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true })
|
||||
customer!: Customer;
|
||||
|
||||
@OneToMany(() => Device, (device) => device.user)
|
||||
devices!: Device[];
|
||||
|
||||
@OneToMany(() => Notification, (notification) => notification.user)
|
||||
notifications!: Notification[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone' })
|
||||
updatedAt!: Date;
|
||||
|
||||
get isPasswordSet(): boolean {
|
||||
return !!this.password;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum GrantType {
|
||||
PASSWORD = 'PASSWORD',
|
||||
BIOMETRIC = 'BIOMETRIC',
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './grant-type.enum';
|
||||
export * from './roles.enum';
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum Roles {
|
||||
JUNIOR = 'JUNIOR',
|
||||
GUARDIAN = 'GUARDIAN',
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './jwt-payload.interface';
|
||||
export * from './login-response.interface';
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Roles } from '../enums';
|
||||
|
||||
export interface IJwtPayload {
|
||||
sub: string;
|
||||
roles: Roles[];
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface ILoginResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
import { Device } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
export class DeviceRepository {
|
||||
constructor(@InjectRepository(Device) private readonly deviceRepository: Repository<Device>) {}
|
||||
|
||||
findUserDeviceById(deviceId: string, userId: string) {
|
||||
return this.deviceRepository.findOne({ where: { deviceId, userId } });
|
||||
}
|
||||
|
||||
createDevice(data: Partial<Device>) {
|
||||
return this.deviceRepository.save(data);
|
||||
}
|
||||
|
||||
updateDevice(deviceId: string, data: Partial<Device>) {
|
||||
return this.deviceRepository.save({ deviceId, ...data });
|
||||
}
|
||||
|
||||
getTokens(userId: string) {
|
||||
return this.deviceRepository.find({ where: { userId, fcmToken: Not(IsNull()) }, select: ['fcmToken'] });
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './device.repository';
|
||||
export * from './user.repository';
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { FindOptionsWhere, Repository } from 'typeorm';
|
||||
import { User } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {}
|
||||
|
||||
createUnverifiedUser(data: Partial<User>) {
|
||||
return this.userRepository.save(
|
||||
this.userRepository.create({
|
||||
phoneNumber: data.phoneNumber,
|
||||
countryCode: data.countryCode,
|
||||
roles: data.roles,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findOne(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||
return this.userRepository.findOne({ where });
|
||||
}
|
||||
|
||||
update(userId: string, data: Partial<User>) {
|
||||
return this.userRepository.update(userId, data);
|
||||
}
|
||||
|
||||
createUser(data: Partial<User>) {
|
||||
const user = this.userRepository.create({
|
||||
...data,
|
||||
});
|
||||
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { Request } from 'express';
|
||||
import { CacheService } from '~/common/modules/cache/services';
|
||||
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
|
||||
import { OtpService } from '~/common/modules/otp/services';
|
||||
import { JuniorTokenService } from '~/junior/services';
|
||||
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
|
||||
import {
|
||||
CreateUnverifiedUserRequestDto,
|
||||
DisableBiometricRequestDto,
|
||||
EnableBiometricRequestDto,
|
||||
ForgetPasswordRequestDto,
|
||||
LoginRequestDto,
|
||||
SendForgetPasswordOtpRequestDto,
|
||||
SetEmailRequestDto,
|
||||
setJuniorPasswordRequestDto,
|
||||
} from '../dtos/request';
|
||||
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
|
||||
import { User } from '../entities';
|
||||
import { GrantType, Roles } from '../enums';
|
||||
import { IJwtPayload, ILoginResponse } from '../interfaces';
|
||||
import { removePadding, verifySignature } from '../utils';
|
||||
import { DeviceService } from './device.service';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
const ONE_THOUSAND = 1000;
|
||||
const SALT_ROUNDS = 10;
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly otpService: OtpService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly userService: UserService,
|
||||
private readonly deviceService: DeviceService,
|
||||
private readonly juniorTokenService: JuniorTokenService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
||||
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
|
||||
|
||||
return this.otpService.generateAndSendOtp({
|
||||
userId: user.id,
|
||||
recipient: user.phoneNumber,
|
||||
scope: OtpScope.VERIFY_PHONE,
|
||||
otpType: OtpType.SMS,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> {
|
||||
const user = await this.userService.findUserOrThrow({ phoneNumber: verifyUserDto.phoneNumber });
|
||||
|
||||
if (user.isPasswordSet) {
|
||||
throw new BadRequestException('USERS.PHONE_ALREADY_VERIFIED');
|
||||
}
|
||||
|
||||
const isOtpValid = await this.otpService.verifyOtp({
|
||||
userId: user.id,
|
||||
scope: OtpScope.VERIFY_PHONE,
|
||||
otpType: OtpType.SMS,
|
||||
value: verifyUserDto.otp,
|
||||
});
|
||||
|
||||
if (!isOtpValid) {
|
||||
throw new BadRequestException('USERS.INVALID_OTP');
|
||||
}
|
||||
|
||||
const updatedUser = await this.userService.verifyUserAndCreateCustomer(user);
|
||||
|
||||
const tokens = await this.generateAuthToken(updatedUser);
|
||||
|
||||
return [tokens, updatedUser];
|
||||
}
|
||||
|
||||
async setEmail(userId: string, { email }: SetEmailRequestDto) {
|
||||
const user = await this.userService.findUserOrThrow({ id: userId });
|
||||
|
||||
if (user.email) {
|
||||
throw new BadRequestException('USERS.EMAIL_ALREADY_SET');
|
||||
}
|
||||
|
||||
const existingUser = await this.userService.findUser({ email });
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('USERS.EMAIL_ALREADY_TAKEN');
|
||||
}
|
||||
|
||||
return this.userService.setEmail(userId, email);
|
||||
}
|
||||
|
||||
async setPasscode(userId: string, passcode: string) {
|
||||
const user = await this.userService.findUserOrThrow({ id: userId });
|
||||
|
||||
if (user.password) {
|
||||
throw new BadRequestException('USERS.PASSCODE_ALREADY_SET');
|
||||
}
|
||||
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||
const hashedPasscode = bcrypt.hashSync(passcode, salt);
|
||||
|
||||
await this.userService.setPasscode(userId, hashedPasscode, salt);
|
||||
}
|
||||
|
||||
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
|
||||
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
|
||||
|
||||
if (!device) {
|
||||
return this.deviceService.createDevice({
|
||||
deviceId,
|
||||
userId,
|
||||
publicKey,
|
||||
});
|
||||
}
|
||||
|
||||
if (device.publicKey) {
|
||||
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_ENABLED');
|
||||
}
|
||||
|
||||
return this.deviceService.updateDevice(deviceId, { publicKey });
|
||||
}
|
||||
|
||||
async disableBiometric(userId: string, { deviceId }: DisableBiometricRequestDto) {
|
||||
const device = await this.deviceService.findUserDeviceById(deviceId, userId);
|
||||
|
||||
if (!device) {
|
||||
throw new BadRequestException('AUTH.DEVICE_NOT_FOUND');
|
||||
}
|
||||
|
||||
if (!device.publicKey) {
|
||||
throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_DISABLED');
|
||||
}
|
||||
|
||||
return this.deviceService.updateDevice(deviceId, { publicKey: null });
|
||||
}
|
||||
|
||||
async sendForgetPasswordOtp({ email }: SendForgetPasswordOtpRequestDto) {
|
||||
const user = await this.userService.findUserOrThrow({ email });
|
||||
|
||||
if (!user.isProfileCompleted) {
|
||||
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
|
||||
}
|
||||
|
||||
return this.otpService.generateAndSendOtp({
|
||||
userId: user.id,
|
||||
recipient: user.email,
|
||||
scope: OtpScope.FORGET_PASSWORD,
|
||||
otpType: OtpType.EMAIL,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyForgetPasswordOtp({ email, otp, password, confirmPassword }: ForgetPasswordRequestDto) {
|
||||
const user = await this.userService.findUserOrThrow({ email });
|
||||
if (!user.isProfileCompleted) {
|
||||
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
|
||||
}
|
||||
const isOtpValid = await this.otpService.verifyOtp({
|
||||
userId: user.id,
|
||||
scope: OtpScope.FORGET_PASSWORD,
|
||||
otpType: OtpType.EMAIL,
|
||||
value: otp,
|
||||
});
|
||||
|
||||
if (!isOtpValid) {
|
||||
throw new BadRequestException('USERS.INVALID_OTP');
|
||||
}
|
||||
|
||||
this.validatePassword(password, confirmPassword, user);
|
||||
|
||||
const hashedPassword = bcrypt.hashSync(password, user.salt);
|
||||
|
||||
await this.userService.setPasscode(user.id, hashedPassword, user.salt);
|
||||
}
|
||||
|
||||
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
|
||||
const user = await this.userService.findUser({ email: loginDto.email });
|
||||
let tokens;
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
if (loginDto.grantType === GrantType.PASSWORD) {
|
||||
tokens = await this.loginWithPassword(loginDto, user);
|
||||
} else {
|
||||
tokens = await this.loginWithBiometric(loginDto, user, deviceId);
|
||||
}
|
||||
|
||||
this.deviceService.updateDevice(deviceId, {
|
||||
lastAccessOn: new Date(),
|
||||
fcmToken: loginDto.fcmToken,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return [tokens, user];
|
||||
}
|
||||
|
||||
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
|
||||
const juniorId = await this.juniorTokenService.validateToken(body.qrToken);
|
||||
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
|
||||
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
|
||||
await this.userService.setPasscode(juniorId, hashedPasscode, salt);
|
||||
await this.juniorTokenService.invalidateToken(body.qrToken);
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<[ILoginResponse, User]> {
|
||||
try {
|
||||
const isValid = await this.jwtService.verifyAsync<IJwtPayload>(refreshToken, {
|
||||
secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
|
||||
});
|
||||
|
||||
const user = await this.userService.findUserOrThrow({ id: isValid.sub });
|
||||
|
||||
const tokens = await this.generateAuthToken(user);
|
||||
|
||||
return [tokens, user];
|
||||
} catch (error) {
|
||||
throw new BadRequestException('AUTH.INVALID_REFRESH_TOKEN');
|
||||
}
|
||||
}
|
||||
|
||||
logout(req: Request) {
|
||||
const accessToken = req.headers.authorization?.split(' ')[1] as string;
|
||||
const expiryInTtl = this.jwtService.decode(accessToken).exp - Date.now() / ONE_THOUSAND;
|
||||
return this.cacheService.set(accessToken, 'LOGOUT', expiryInTtl);
|
||||
}
|
||||
|
||||
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
|
||||
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
const tokens = await this.generateAuthToken(user);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private async loginWithBiometric(loginDto: LoginRequestDto, user: User, deviceId: string): Promise<ILoginResponse> {
|
||||
const device = await this.deviceService.findUserDeviceById(deviceId, user.id);
|
||||
|
||||
if (!device) {
|
||||
throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND');
|
||||
}
|
||||
|
||||
if (!device.publicKey) {
|
||||
throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED');
|
||||
}
|
||||
|
||||
const cleanToken = removePadding(loginDto.signature);
|
||||
const isValidToken = await verifySignature(
|
||||
device.publicKey,
|
||||
cleanToken,
|
||||
`${user.email} - ${device.deviceId}`,
|
||||
'SHA1',
|
||||
);
|
||||
|
||||
if (!isValidToken) {
|
||||
throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC');
|
||||
}
|
||||
|
||||
const tokens = await this.generateAuthToken(user);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private async generateAuthToken(user: User) {
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.sign(
|
||||
{ sub: user.id, roles: user.roles },
|
||||
{
|
||||
expiresIn: this.configService.getOrThrow('JWT_ACCESS_TOKEN_EXPIRY'),
|
||||
secret: this.configService.getOrThrow('JWT_ACCESS_TOKEN_SECRET'),
|
||||
},
|
||||
),
|
||||
this.jwtService.sign(
|
||||
{ sub: user.id, roles: user.roles },
|
||||
{
|
||||
expiresIn: this.configService.getOrThrow('JWT_REFRESH_TOKEN_EXPIRY'),
|
||||
secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
|
||||
}
|
||||
|
||||
private validatePassword(password: string, confirmPassword: string, user: User) {
|
||||
if (password !== confirmPassword) {
|
||||
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
|
||||
}
|
||||
|
||||
const roles = user.roles;
|
||||
|
||||
if (roles.includes(Roles.GUARDIAN) && !PASSCODE_REGEX.test(password)) {
|
||||
throw new BadRequestException('AUTH.INVALID_PASSCODE');
|
||||
}
|
||||
|
||||
if (roles.includes(Roles.JUNIOR) && !PASSWORD_REGEX.test(password)) {
|
||||
throw new BadRequestException('AUTH.INVALID_PASSWORD');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Device } from '../entities';
|
||||
import { DeviceRepository } from '../repositories';
|
||||
@Injectable()
|
||||
export class DeviceService {
|
||||
constructor(private readonly deviceRepository: DeviceRepository) {}
|
||||
findUserDeviceById(deviceId: string, userId: string) {
|
||||
return this.deviceRepository.findUserDeviceById(deviceId, userId);
|
||||
}
|
||||
|
||||
createDevice(data: Partial<Device>) {
|
||||
return this.deviceRepository.createDevice(data);
|
||||
}
|
||||
|
||||
updateDevice(deviceId: string, data: Partial<Device>) {
|
||||
return this.deviceRepository.updateDevice(deviceId, data);
|
||||
}
|
||||
|
||||
async getTokens(userId: string): Promise<string[]> {
|
||||
const devices = await this.deviceRepository.getTokens(userId);
|
||||
|
||||
return devices.map((device) => device.fcmToken!);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './auth.service';
|
||||
export * from './device.service';
|
||||
export * from './user.service';
|
||||
@@ -1,75 +0,0 @@
|
||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { Guardian } from '~/guardian/entities/guradian.entity';
|
||||
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
|
||||
import { User } from '../entities';
|
||||
import { Roles } from '../enums';
|
||||
import { UserRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
|
||||
) {}
|
||||
|
||||
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
|
||||
return this.userRepository.findOne(where);
|
||||
}
|
||||
|
||||
async findUserOrThrow(where: FindOptionsWhere<User>) {
|
||||
const user = await this.findUser(where);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('USERS.NOT_FOUND');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findOrCreateUser({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
|
||||
const user = await this.userRepository.findOne({ phoneNumber });
|
||||
|
||||
if (!user) {
|
||||
return this.userRepository.createUnverifiedUser({ phoneNumber, countryCode, roles: [Roles.GUARDIAN] });
|
||||
}
|
||||
if (user && user.roles.includes(Roles.GUARDIAN) && user.isPasswordSet) {
|
||||
throw new BadRequestException('USERS.PHONE_NUMBER_ALREADY_EXISTS');
|
||||
}
|
||||
|
||||
if (user && user.roles.includes(Roles.JUNIOR)) {
|
||||
throw new BadRequestException('USERS.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
|
||||
//TODO add role Guardian to the existing user and send OTP
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(data: Partial<User>) {
|
||||
const user = await this.userRepository.createUser(data);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
setEmail(userId: string, email: string) {
|
||||
return this.userRepository.update(userId, { email });
|
||||
}
|
||||
|
||||
setPasscode(userId: string, passcode: string, salt: string) {
|
||||
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
|
||||
}
|
||||
|
||||
async verifyUserAndCreateCustomer(user: User) {
|
||||
await this.customerService.createCustomer(
|
||||
{
|
||||
guardian: Guardian.create({ id: user.id }),
|
||||
notificationSettings: new CustomerNotificationSettings(),
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
return this.findUserOrThrow({ id: user.id });
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { IJwtPayload } from '../interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenStrategy extends PassportStrategy(Strategy, 'access-token') {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('JWT_ACCESS_TOKEN_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: IJwtPayload) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './access-token.strategy';
|
||||
@@ -1,26 +0,0 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export function verifySignature(
|
||||
publicKeyBase64: string,
|
||||
signatureBase64: string,
|
||||
message: string,
|
||||
algorithm: 'SHA1' | 'SHA384',
|
||||
) {
|
||||
const signatureBuffer = Buffer.from(signatureBase64, 'base64');
|
||||
|
||||
const publicKeyPEM = '-----BEGIN PUBLIC KEY-----\n' + publicKeyBase64 + '\n-----END PUBLIC KEY-----';
|
||||
const verifier = crypto.createVerify(algorithm);
|
||||
verifier.update(message, 'utf8');
|
||||
return verifier.verify(
|
||||
{
|
||||
key: publicKeyPEM,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING,
|
||||
},
|
||||
signatureBuffer,
|
||||
);
|
||||
}
|
||||
|
||||
export function removePadding(originalSignature: string) {
|
||||
const buffer = Buffer.from(originalSignature, 'base64');
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './crypt';
|
||||
@@ -1 +0,0 @@
|
||||
export const DEVICE_ID_HEADER = 'x-client-id';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './global.constant';
|
||||
@@ -1,5 +0,0 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Roles } from '~/auth/enums';
|
||||
|
||||
export const ROLE_METADATA_KEY = 'roles';
|
||||
export const AllowedRoles = (...roles: Roles[]) => SetMetadata(ROLE_METADATA_KEY, roles);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './allowed-roles.decorator';
|
||||
export * from './public.decorator';
|
||||
export * from './user.decorator';
|
||||
@@ -1,4 +0,0 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const AuthenticatedUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
|
||||
const req = ctx.switchToHttp().getRequest();
|
||||
return req.user;
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { IS_PUBLIC_KEY } from '../decorators';
|
||||
import { CacheService } from '../modules/cache/services';
|
||||
|
||||
@Injectable()
|
||||
export class AccessTokenGuard extends AuthGuard('access-token') {
|
||||
constructor(protected reflector: Reflector, private readonly cacheService: CacheService) {
|
||||
super();
|
||||
}
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await super.canActivate(context);
|
||||
|
||||
const token = context.switchToHttp().getRequest().headers['authorization']?.split(' ')[1];
|
||||
|
||||
const isRevoked = await this.cacheService.get(token);
|
||||
|
||||
if (isRevoked) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './access-token.guard';
|
||||
export * from './roles-guard';
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Roles } from '~/auth/enums';
|
||||
import { ROLE_METADATA_KEY } from '../decorators';
|
||||
import { AccessTokenGuard } from './access-token.guard';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard extends AccessTokenGuard {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
await super.canActivate(context);
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
const allowedRoles = this.reflector.getAllAndOverride<Roles[]>(ROLE_METADATA_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!allowedRoles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return allowedRoles.some((role) => user.roles.includes(role));
|
||||
}
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { buildKeyvOptions } from '~/core/module-options';
|
||||
import { CacheService } from './services';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'CACHE_INSTANCE',
|
||||
useFactory: (config: ConfigService) => buildKeyvOptions(config),
|
||||
inject: [ConfigService],
|
||||
},
|
||||
CacheService,
|
||||
],
|
||||
exports: ['CACHE_INSTANCE', CacheService],
|
||||
})
|
||||
@Global()
|
||||
export class CacheModule {}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cacheable } from 'cacheable';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
constructor(@Inject('CACHE_INSTANCE') private readonly cache: Cacheable) {}
|
||||
|
||||
get<T>(key: string): Promise<T | undefined> {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number | string): Promise<void> {
|
||||
await this.cache.set(key, value, ttl);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
export * from './cache.services';
|
||||
@@ -1 +0,0 @@
|
||||
export * from './lookup.controller';
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ApiDataArrayResponse } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { DocumentMetaResponseDto } from '~/document/dtos/response';
|
||||
import { LookupService } from '../services';
|
||||
|
||||
@Controller('lookup')
|
||||
@ApiTags('Lookups')
|
||||
@ApiBearerAuth()
|
||||
export class LookupController {
|
||||
constructor(private readonly lookupService: LookupService) {}
|
||||
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@Get('default-avatars')
|
||||
@ApiDataArrayResponse(DocumentMetaResponseDto)
|
||||
async findDefaultAvatars() {
|
||||
const avatars = await this.lookupService.findDefaultAvatar();
|
||||
|
||||
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
|
||||
}
|
||||
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@Get('default-task-logos')
|
||||
@ApiDataArrayResponse(DocumentMetaResponseDto)
|
||||
async findDefaultTaskLogos() {
|
||||
const avatars = await this.lookupService.findDefaultTasksLogo();
|
||||
|
||||
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DocumentModule } from '~/document/document.module';
|
||||
import { LookupController } from './controllers';
|
||||
import { LookupService } from './services';
|
||||
|
||||
@Module({
|
||||
controllers: [LookupController],
|
||||
providers: [LookupService],
|
||||
imports: [DocumentModule],
|
||||
})
|
||||
export class LookupModule {}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './lookup.service';
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DocumentType } from '~/document/enums';
|
||||
import { DocumentService, OciService } from '~/document/services';
|
||||
|
||||
@Injectable()
|
||||
export class LookupService {
|
||||
constructor(private readonly documentService: DocumentService, private readonly ociService: OciService) {}
|
||||
async findDefaultAvatar() {
|
||||
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR });
|
||||
|
||||
await Promise.all(
|
||||
documents.map(async (document) => {
|
||||
document.url = await this.ociService.generatePreSignedUrl(document);
|
||||
}),
|
||||
);
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
async findDefaultTasksLogo() {
|
||||
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_TASKS_LOGO });
|
||||
|
||||
await Promise.all(
|
||||
documents.map(async (document) => {
|
||||
document.url = await this.ociService.generatePreSignedUrl(document);
|
||||
}),
|
||||
);
|
||||
|
||||
return documents;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './notifications.controller';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user