chore: first
This commit is contained in:
Executable
+47
@@ -0,0 +1,47 @@
|
||||
name: Build and Deploy Docker Images
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DANAK_SERVER: "https://captain.dev.danakcorp.com"
|
||||
APP_TOKEN: 1b5da4107e67bb72c17bcecdaea51e6ccd0c545a4e7ffbcf4fd7e2c83bfe24b0
|
||||
APP_NAME: danak-api
|
||||
GITHUB_TOKEN: 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
|
||||
|
||||
steps:
|
||||
- name: Check out repositorys
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: zmihamid
|
||||
password: ${{ env.GITHUB_TOKEN }}
|
||||
|
||||
- name: Preset Image Name
|
||||
run: echo "IMAGE_URL=$(echo ghcr.io/zmihamid/${{ github.event.repository.name }}:$(echo ${{ github.sha }} | cut -c1-7) | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.IMAGE_URL }}
|
||||
|
||||
- name: Install CapRover CLI
|
||||
run: npm install -g caprover
|
||||
|
||||
- name: deploy to server
|
||||
run: |
|
||||
caprover deploy -a $APP_NAME -u $DANAK_SERVER --appToken $APP_TOKEN -i "$IMAGE_URL"
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# 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
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npx --no-install commitlint --edit "$1"
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
pnpm dlx lint-staged
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"endOfLine": "auto",
|
||||
"printWidth": 140,
|
||||
"arrowParens": "always",
|
||||
"semi": true,
|
||||
"tabWidth": 2
|
||||
}
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
# Stage 1: Build Stage
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
RUN npm install -g corepack@latest
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate
|
||||
|
||||
# Install tzdata to support timezone settings
|
||||
RUN apk add --no-cache tzdata
|
||||
|
||||
# Set the timezone to Asia/Tehran
|
||||
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||
|
||||
|
||||
FROM base AS deps
|
||||
WORKDIR /temp-deps
|
||||
COPY package*.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile --loglevel info
|
||||
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /build
|
||||
COPY . ./
|
||||
COPY --from=deps /temp-deps/node_modules ./node_modules
|
||||
RUN if [ -f package.json ] && grep -q '"build":' package.json; then pnpm run build; fi
|
||||
RUN pnpm install --prod --frozen-lockfile --loglevel info
|
||||
|
||||
FROM base AS runner
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
COPY --from=builder --chown=appuser:appgroup /build/ ./
|
||||
|
||||
RUN chown -R appuser:appgroup /app
|
||||
|
||||
USER appuser
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3500
|
||||
|
||||
CMD ["npm", "start"]
|
||||
@@ -0,0 +1,98 @@
|
||||
<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://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
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ pnpm run start
|
||||
|
||||
# watch mode
|
||||
$ pnpm run start:dev
|
||||
|
||||
# production mode
|
||||
$ pnpm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ pnpm run test
|
||||
|
||||
# e2e tests
|
||||
$ pnpm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ pnpm 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
|
||||
$ pnpm install -g @nestjs/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).
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
rules: {
|
||||
"subject-case": [2, "always", ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case", "camel-case"]],
|
||||
"type-enum": [2, "always", ["add", "build", "chore", "update", "docs", "feat", "fix", "refactor", "revert", "style", "test", "sample"]],
|
||||
"scope-enum": [1, "always", ["common", "core", "app", "testing", "bug"]],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Options } from "@mikro-orm/core";
|
||||
import { PostgreSqlDriver } from "@mikro-orm/postgresql";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const config: Options = {
|
||||
driver: PostgreSqlDriver,
|
||||
dbName: process.env.DB_NAME,
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
entities: ["./dist/**/*.entity.js"],
|
||||
entitiesTs: ["./src/**/*.entity.ts"],
|
||||
debug: process.env.NODE_ENV !== "production",
|
||||
migrations: {
|
||||
path: "./database/migrations",
|
||||
pathTs: "./database/migrations",
|
||||
tableName: "migrations",
|
||||
transactional: true,
|
||||
allOrNothing: true,
|
||||
dropTables: false,
|
||||
safe: true,
|
||||
emit: "ts",
|
||||
},
|
||||
seeder: {
|
||||
path: "./database/seeders",
|
||||
pathTs: "./database/seeders",
|
||||
defaultSeeder: "IndexSeeder",
|
||||
glob: "!(*.d).{js,ts}",
|
||||
emit: "ts",
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
pg_db:
|
||||
image: postgres:alpine
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
container_name: dzone-pg-db
|
||||
ports:
|
||||
- ${DB_PORT}:5432
|
||||
# networks:
|
||||
# - app_network
|
||||
volumes:
|
||||
- dzone-pg-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASS}
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_DB: ${DB_NAME}
|
||||
|
||||
volumes:
|
||||
dzone-pg-data:
|
||||
@@ -0,0 +1,130 @@
|
||||
import eslint from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import eslintPluginPrettier from "eslint-plugin-prettier";
|
||||
import eslintPluginImport from "eslint-plugin-import";
|
||||
import globals from "globals";
|
||||
import prettierConfig from "eslint-config-prettier";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"jest.config.js",
|
||||
"coverage/**",
|
||||
"database/migrations/**",
|
||||
"pnpm-lock.yaml",
|
||||
".prettierrc.js",
|
||||
"commitlint.config.ts",
|
||||
"eslint.config.mjs",
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettierConfig,
|
||||
// Base configuration for all TypeScript files
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
prettier: eslintPluginPrettier,
|
||||
import: eslintPluginImport,
|
||||
},
|
||||
rules: {
|
||||
// Import rules
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
ignoreCase: false,
|
||||
ignoreDeclarationSort: true,
|
||||
ignoreMemberSort: false,
|
||||
memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
|
||||
allowSeparatedGroups: true,
|
||||
},
|
||||
],
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
groups: ["builtin", "external", "internal", ["sibling", "parent"], "index", "unknown"],
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// JavaScript Rules
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"space-in-parens": "error",
|
||||
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 0 }],
|
||||
semi: ["error", "always"],
|
||||
|
||||
// Best Practices
|
||||
"no-async-promise-executor": "warn",
|
||||
"no-control-regex": "warn",
|
||||
"no-empty": ["warn", { allowEmptyCatch: true }],
|
||||
"no-extra-semi": "warn",
|
||||
"no-prototype-builtins": "warn",
|
||||
"no-regex-spaces": "warn",
|
||||
"prefer-rest-params": "warn",
|
||||
"prefer-spread": "warn",
|
||||
|
||||
// TypeScript-Specific Rules that don't need type information
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-loss-of-precision": "warn",
|
||||
"@typescript-eslint/no-misused-new": "warn",
|
||||
"@typescript-eslint/no-namespace": "warn",
|
||||
"@typescript-eslint/no-this-alias": "warn",
|
||||
"@typescript-eslint/no-unnecessary-type-constraint": "warn",
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-var-requires": "warn",
|
||||
"@typescript-eslint/triple-slash-reference": "warn",
|
||||
},
|
||||
},
|
||||
// Additional configuration for type-checked files - disabled for now
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
// Disable type-aware linting until we can configure it properly
|
||||
rules: {
|
||||
"@typescript-eslint/await-thenable": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/unbound-method": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"name": "dzone-api",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start:nest": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start": "NODE_ENV=production node --trace-warnings dist/main",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --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",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.806.0",
|
||||
"@fastify/static": "^8.1.1",
|
||||
"@keyv/redis": "^4.4.0",
|
||||
"@mikro-orm/core": "^6.4.15",
|
||||
"@mikro-orm/nestjs": "^6.1.1",
|
||||
"@mikro-orm/postgresql": "^6.4.15",
|
||||
"@nest-lab/fastify-multer": "^1.3.0",
|
||||
"@nest-lab/throttler-storage-redis": "^1.1.0",
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/axios": "^4.0.0",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-fastify": "^11.1.0",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"axios": "^1.9.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.52.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"decimal.js": "^10.5.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"fastify": "^5.3.2",
|
||||
"nodemailer": "^7.0.3",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"slugify": "^1.6.6",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.0",
|
||||
"@commitlint/config-conventional": "^19.8.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.31.0",
|
||||
"@typescript-eslint/parser": "^8.31.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-import-resolver-typescript": "^3.10.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"globals": "^16.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"prettier": "^3.5.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.31.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.ts": [
|
||||
"pnpm format",
|
||||
"pnpm lint:fix",
|
||||
"git add ."
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531"
|
||||
}
|
||||
Generated
+13911
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { CacheModule } from "@nestjs/cache-manager";
|
||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { ThrottlerModule } from "@nestjs/throttler";
|
||||
import { MailerModule } from "@nestjs-modules/mailer";
|
||||
|
||||
import { bullMqConfig } from "./configs/bullmq.config";
|
||||
import { cacheConfig } from "./configs/cache.config";
|
||||
import { mailerConfig } from "./configs/mailer.config";
|
||||
import { databaseConfig } from "./configs/mikro-orm.config";
|
||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||
import { HTTPLogger } from "./core/middlewares/logger.middleware";
|
||||
import { AuthModule } from "./modules/auth/auth.module";
|
||||
import { NotificationModule } from "./modules/notifications/notifications.module";
|
||||
import { UsersModule } from "./modules/users/users.module";
|
||||
import { UtilsModule } from "./modules/utils/utils.module";
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true, cache: true }),
|
||||
MikroOrmModule.forRootAsync(databaseConfig),
|
||||
BullModule.forRootAsync(bullMqConfig()),
|
||||
MailerModule.forRootAsync(mailerConfig()),
|
||||
ThrottlerModule.forRootAsync(rateLimitConfig()),
|
||||
CacheModule.registerAsync(cacheConfig()),
|
||||
HttpModule.register({ global: true, timeout: 10000, headers: { "Content-Type": "application/json" } }),
|
||||
FastifyMulterModule.register({
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024,
|
||||
fieldNameSize: 200,
|
||||
fieldSize: 10 * 1024 * 1024,
|
||||
fields: 10,
|
||||
parts: 10,
|
||||
},
|
||||
}),
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
NotificationModule,
|
||||
UtilsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(HTTPLogger).forRoutes("*");
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNotEmpty, IsNumber, IsOptional, Max, Min } from "class-validator";
|
||||
|
||||
export class PaginationDto {
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ type: "number", required: false })
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
@Type(() => Number)
|
||||
@ApiPropertyOptional({ type: "number", required: false })
|
||||
limit?: number;
|
||||
}
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
import { ApiProperty, PartialType } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsUUID } from "class-validator";
|
||||
|
||||
import { CommonMessage } from "../enums/message.enum";
|
||||
|
||||
export class ParamDto {
|
||||
@IsNotEmpty({ message: CommonMessage.ID_REQUIRED })
|
||||
@IsUUID("7", { message: CommonMessage.ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Id of the entity", example: "8b1e8b1e-8b1e-8b1e-8b1e-8b1e8b1e8b1e" })
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class OptionalParamDto extends PartialType(ParamDto) {}
|
||||
|
||||
export class ParamNumberIdDto {
|
||||
@IsNotEmpty({ message: CommonMessage.ID_REQUIRED })
|
||||
@ApiProperty({ description: "Id of the entity", example: "20" })
|
||||
id: string;
|
||||
}
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
||||
export const AUTH_THROTTLE_TTL = 5 * 60 * 1000;
|
||||
export const AUTH_THROTTLE_LIMIT = 5;
|
||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
|
||||
export const JWT_STRATEGY_NAME = "jwt_Strategy";
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
export const ADMIN_ROUTE = "shouldAdmin";
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
export const AdminRoute = (isAdminRoute: boolean = true) => SetMetadata(ADMIN_ROUTE, isAdminRoute);
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { UseGuards, applyDecorators } from "@nestjs/common";
|
||||
import { ApiBearerAuth } from "@nestjs/swagger";
|
||||
|
||||
import { AdminRouteGuard } from "../../modules/auth/guards/admin.guard";
|
||||
import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard";
|
||||
// import { PermissionsGuard } from "../../modules/auth/guards/permission.guard";
|
||||
// import { RoleGuard } from "../../modules/auth/guards/role.guard";
|
||||
|
||||
export function AuthGuards() {
|
||||
return applyDecorators(UseGuards(JwtAuthGuard, AdminRouteGuard), ApiBearerAuth("authorization"));
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
registerDecorator,
|
||||
} from "class-validator";
|
||||
|
||||
export function isValidIranianNationalCode(nationalCode: string): boolean {
|
||||
if (!/^\d{10}$/.test(nationalCode)) return false;
|
||||
|
||||
const digits = nationalCode.split("").map(Number);
|
||||
const controlDigit = digits[9];
|
||||
const sum = digits.slice(0, 9).reduce((acc, digit, index) => acc + digit * (10 - index), 0);
|
||||
const remainder = sum % 11;
|
||||
|
||||
return (remainder < 2 && controlDigit === remainder) || (remainder >= 2 && controlDigit === 11 - remainder);
|
||||
}
|
||||
|
||||
@ValidatorConstraint({ async: false })
|
||||
export class IsNationalCodeConstraint implements ValidatorConstraintInterface {
|
||||
validate(value: unknown, _args: ValidationArguments) {
|
||||
return typeof value === "string" && isValidIranianNationalCode(value);
|
||||
}
|
||||
|
||||
defaultMessage(_args: ValidationArguments) {
|
||||
return `Invalid Iranian national code :${_args.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function IsNationalCode(validationOptions?: ValidationOptions) {
|
||||
return function (object: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsNationalCodeConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { applyDecorators } from "@nestjs/common";
|
||||
import { ApiQuery } from "@nestjs/swagger";
|
||||
|
||||
export function Pagination() {
|
||||
return applyDecorators(
|
||||
ApiQuery({ name: "page", example: 1, required: false, type: "number" }),
|
||||
ApiQuery({ name: "limit", example: 10, required: false, type: "number" }),
|
||||
);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
// import { PermissionEnum } from "../../modules/users/enums/permission.enum";
|
||||
|
||||
// export const PERMISSION_KEY = "permissions";
|
||||
// export const PermissionsDec = (...permissions: PermissionEnum[]) => SetMetadata(PERMISSION_KEY, permissions);
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
// import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
// export const ROLES_KEY = "roles";
|
||||
// export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
export const SKIP_AUTH_KEY = "skipAuth";
|
||||
|
||||
export const SkipAuth = () => SetMetadata(SKIP_AUTH_KEY, true);
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { ExecutionContext, createParamDecorator } from "@nestjs/common";
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { ITokenPayload } from "../../modules/auth/interfaces/IToken-payload";
|
||||
|
||||
declare module "fastify" {
|
||||
interface FastifyRequest {
|
||||
// user?: Omit<User, "password">;
|
||||
user?: ITokenPayload;
|
||||
}
|
||||
}
|
||||
|
||||
export const UserDec = createParamDecorator((data: keyof ITokenPayload | undefined, ctx: ExecutionContext) => {
|
||||
const req = ctx.switchToHttp().getRequest<FastifyRequest>();
|
||||
const user = req.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
});
|
||||
|
||||
// export const UserDec = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
|
||||
// const req = ctx.switchToHttp().getRequest<FastifyRequest>();
|
||||
// return req.user;
|
||||
// });
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
import { Opt, PrimaryKey, Property, sql } from "@mikro-orm/core";
|
||||
import { v7 } from "uuid";
|
||||
|
||||
export abstract class BaseEntity {
|
||||
@PrimaryKey({ type: "uuid" })
|
||||
id = v7();
|
||||
|
||||
@Property({ default: sql.now(), type: "timestamptz" })
|
||||
createdAt: Date & Opt;
|
||||
|
||||
@Property({ default: sql.now(), type: "timestamptz" })
|
||||
updatedAt: Date & Opt;
|
||||
|
||||
@Property({ nullable: true })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
Executable
+525
@@ -0,0 +1,525 @@
|
||||
export const enum AuthMessage {
|
||||
UNAUTHORIZED_ACCESS = "شما دسترسی لازم برای این عملیات را ندارید.",
|
||||
PHONE_REGISTERED = "شماره ثبت شد",
|
||||
INVALID_PHONE_FORMAT = "فرمت شماره تلفن صحیح نیست",
|
||||
OTP_SENT = "کد یکبار مصرف به شماره شما ارسال شد.",
|
||||
INVALID_PHONE = "شماره موبایل اشتباه می باشد",
|
||||
INVALID_CREDENTIAL = "شماره موبایل یا رمز عبور اشتباه می باشد",
|
||||
OTP_FAILED = "کد یا شماره موبایل اشتباه است",
|
||||
LOGIN_SUCCESS = "ورود با موفقیت انجام شد",
|
||||
PASSWORD_LOGIN_SUCCESS = "با موفقیت وارد شدید",
|
||||
FORGOT_PASSWORD_SUCCESS = "درخواست فراموشی رمز عبور با موفقیت ثبت شد",
|
||||
PASSWORD_SET_SUCCESS = "پسورد با موفقیت تنظیم شد",
|
||||
PASSWORD_UPDATE_SUCCESS = "رمز عبور شما تغییر کرد",
|
||||
INVALID_OTP = "کد صحیح نمیباشد یا منقضی شده است",
|
||||
PASSWORD_MISMATCH = "رمز عبور یکسان نیست",
|
||||
INVALID_PASSWORD = "ایمیل یا رمز عبور اشتباه است",
|
||||
INVALID_REPEAT_PASSWORD = "تکرار رمز عبور اشتباه است",
|
||||
EMAIL_NOT_EMPTY = "ایمیل نمیتواند خالی باشد.",
|
||||
INVALID_EMAIL_FORMAT = "فرمت ایمیل صحیح نیست",
|
||||
PasswordNotEmpty = "پسورد نمیتواند خالی باشد.",
|
||||
USER_NOT_FOUND = "کاربری با این شماره وجود ندارد",
|
||||
USER_EXISTS = "با این شماره قبلا ثبت نام شده است",
|
||||
USER_REGISTER_SUCCESS = "ثبت نام موفقیت آمیز بود",
|
||||
INVALID_USER = "کاربری با این مشخصات یافت نشد",
|
||||
PHONE_NOT_FOUND = "کاربری با این شماره یافت نشد",
|
||||
TOKEN_EXPIRED = "توکن منقضی شده است",
|
||||
TOKEN_INVALID = "توکن نامعتبر است",
|
||||
BANNED = "در حال حاضر شما امکان دسترسی به این سرویس را ندارید",
|
||||
PHONE_EXISTS = "شماره تلفن قبلا ثبت شده است",
|
||||
ADMIN_CREATED = "ادمین با موفقیت ایجاد شد",
|
||||
PERM_NOT_FOUND = "دسترسی یافت نشد",
|
||||
INVALID_PASS_FORMAT = "فرمت رمز عبور صحیح نیست",
|
||||
PHONE_NOT_EMPTY = "شماره تلفن نمیتواند خالی باشد",
|
||||
PASSWORD_FORMAT_INVALID = "رمز عبور باید به صورت رشته باشد",
|
||||
OTP_FORMAT_INVALID = "کد یکبار مصرف باید عددی و ۵ رقمی باشد",
|
||||
PHONE_FORMAT_INVALID = "شماره تلفن باید معتبر و به فرمت ایران باشد",
|
||||
TOKEN_NOT_EMPTY = "توکن نمیتواند خالی باشد",
|
||||
PASSWORD_NOT_EMPTY = "رمز عبور نمیتواند خالی باشد",
|
||||
PASSWORD_CHANGED_SUCCESSFULLY = "رمز عبور با موفقیت تغییر کرد",
|
||||
PASSWORD_CONFIRMATION_NOT_EMPTY = "تایید رمز عبور نمیتواند خالی باشد",
|
||||
PASSWORD_LENGTH = "رمز عبور باید حداقل ۸ کاراکتر باشد",
|
||||
OTP_NOT_EMPTY = "کد یکبار مصرف نمیتواند خالی باشد",
|
||||
LAST_PASSWORD_NOT_EMPTY = "رمز عبور قبلی نمیتواند خالی باشد",
|
||||
FIRST_NAME_NOT_EMPTY = "نام نمیتواند خالی باشد",
|
||||
LAST_NAME_NOT_EMPTY = "نام خانوادگی نمیتواند خالی باشد",
|
||||
FIRST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = "نام باید بین ۲ تا ۵۰ کاراکتر باشد",
|
||||
LAST_NAME_SHOULD_BE_BETWEEN_2_AND_50 = "نام خانوادگی باید بین ۲ تا ۵۰ کاراکتر باشد",
|
||||
BIRTH_DATE_NOT_EMPTY = "تاریخ تولد نمیتواند خالی باشد",
|
||||
NATIONAL_CODE_INCORRECT = "کد ملی باید ۱۰ رقمی باشد",
|
||||
NATIONAL_NOT_EMPTY = "کد ملی نمیتواند خالی باشد",
|
||||
OTP_ALREADY_SENT = "کد یکبار مصرف قبلا ارسال شده است",
|
||||
TOO_MANY_REQUESTS = "تعداد درخواست های شما بیش از حد مجاز است",
|
||||
NOT_ADMIN = "شما دسترسی به بخش ادمین ندارید",
|
||||
PHONE_SHOULD_BE_11_DIGIT = "شماره تلفن باید ۱۱ رقم باشد",
|
||||
TIMESTAMP_NOT_EMPTY = "زمان نمیتواند خالی باشد",
|
||||
ADMIN_CAN_NOT_LOGIN = "ادمین نمیتواند وارد شود",
|
||||
NATIONAL_CODE_INVALID = "کد ملی نامعتبر است",
|
||||
INVALID_REFRESH_TOKEN = "توکن رفرش نامعتبر است",
|
||||
REFRESH_TOKEN_EXPIRED = "توکن رفرش منقضی شده است",
|
||||
LOGOUT_SUCCESS = "خروج با موفقیت انجام شد",
|
||||
CURRENT_PASSWORD_INVALID = "رمز عبور فعلی نامعتبر است",
|
||||
TOKEN_EXPIRED_OR_INVALID = "توکن منقضی شده یا نامعتبر است",
|
||||
ACCESS_DENIED = "دسترسی شما محدود شده است",
|
||||
}
|
||||
|
||||
export const enum UserMessage {
|
||||
USER_NOT_FOUND = "کاربری با این مشخصات یافت نشد",
|
||||
EMAIL_EXIST = "ایمیل قبلا ثبت شده است",
|
||||
ROLE_NOT_FOUND = "نقش یافت نشد",
|
||||
USER_EXISTS = "کاربر با این مشخصات وجود دارد",
|
||||
USERNAME_EXIST = "نام کاربری قبلا ثبت شده است",
|
||||
USERNAME_NOT_EMPTY = "نام کاربری نمیتواند خالی باشد",
|
||||
USERNAME_SHOULD_BE_BETWEEN_3_AND_50 = "نام کاربری باید بین ۳ تا ۵۰ کاراکتر باشد",
|
||||
USER_GROUP_NOT_FOUND = "گروه کاربری یافت نشد",
|
||||
USER_GROUP_CREATED = "گروه کاربری با موفقیت ایجاد شد",
|
||||
USER_ID_SHOULD_BE_A_UUID = "شناسه کاربر باید یک UUID باشد",
|
||||
NATIONAL_CODE_EXIST = "کد ملی قبلا ثبت شده است",
|
||||
PROFILE_PIC_URL = "آدرس عکس پروفایل باید یک آدرس یو آر ال باشد",
|
||||
PROFILE_PIC_REQUIRED = "عکس پروفایل نمیتواند خالی باشد",
|
||||
VERIFY_EMAIL_LINK_SENT = "لینک تایید ایمیل به ایمیل شما ارسال شد",
|
||||
EMAIL_VERIFIED = "ایمیل شما تایید شد",
|
||||
INVALID_EMAIL_TOKEN = "توکن ایمیل نامعتبر است یا منقضی شده است",
|
||||
PHONE_EXIST = "شماره تلفن قبلا ثبت شده است",
|
||||
ADDRESS_REQUIRED = "آدرس نمیتواند خالی باشد",
|
||||
ADDRESS_SHOULD_BE_STRING = "آدرس باید یک رشته باشد",
|
||||
ADDRESS_LENGTH = "آدرس باید بین ۵ تا ۲۵۵ کاراکتر باشد",
|
||||
POSTAL_CODE_REQUIRED = "کد پستی نمیتواند خالی باشد",
|
||||
POSTAL_CODE_SHOULD_BE_STRING = "کد پستی باید یک رشته باشد",
|
||||
POSTAL_CODE_LENGTH = "کد پستی باید ۱۰ رقم باشد",
|
||||
CITY_REQUIRED = "شهر نمیتواند خالی باشد",
|
||||
CITY_NOT_FOUND = "شهر یافت نشد",
|
||||
CITY_SHOULD_BE_UUID = "شناسه شهر باید یک UUID باشد",
|
||||
REAL_DATA_EXIST = "اطلاعات حقیقی قبلا ثبت شده است",
|
||||
LEGAL_DATA_EXIST = "اطلاعات حقوقی قبلا ثبت شده است",
|
||||
ECONOMIC_CODE_EXIST = "کد اقتصادی قبلا ثبت شده است",
|
||||
REGISTRATION_CODE_EXIST = "کد ثبت قبلا ثبت شده است",
|
||||
NATIONAL_IDENTITY_EXIST = "شناسه ملی قبلا ثبت شده است",
|
||||
PHONE_SHOULD_BE_11_DIGIT = "شماره تلفن باید ۱۱ رقم باشد",
|
||||
FINANCIAL_TYPE_IS_LEGAL = "نوع کاربر حقوقی میباشد",
|
||||
FINANCIAL_TYPE_IS_REAL = "نوع کاربر حقیقی میباشد",
|
||||
LEGAL_DATA_NOT_FOUND = "اطلاعات حقوقی یافت نشد",
|
||||
REAL_DATA_NOT_FOUND = "اطلاعات حقیقی یافت نشد",
|
||||
REGISTRATION_NAME_EXIST = "نام ثبت قبلا ثبت شده است",
|
||||
ADDRESS_NOT_FOUND = "آدرس یافت نشد",
|
||||
ADMIN_ID_REQUIRED = "شناسه ادمین مورد نیاز است",
|
||||
INVALID_ADMIN_ID = "شناسه ادمین نامعتبر است",
|
||||
ADMIN_ID_SHOULD_BE_UUID = "شناسه ادمین باید یک UUID باشد",
|
||||
ADMIN_NOT_FOUND = "ادمین یافت نشد",
|
||||
PHONE_MUST_BE_A_STRING = "شماره تلفن باید یک رشته باشد",
|
||||
}
|
||||
|
||||
export const enum CommonMessage {
|
||||
VALIDITY_TYPE_REQUIRED = "نوع اعتبار سنجی مورد نیاز است",
|
||||
THIS_FILED_IS_REQUIRED = "این فیلد الزامی است",
|
||||
VALID_FOR_CHOOSE = "معتبر برای انتخاب",
|
||||
UPDATE_SUCCESS = "با موفقیت به روز رسانی شد",
|
||||
CREATED = "با موفقیت ایجاد شد",
|
||||
DELETED = "با موفقیت حذف شد",
|
||||
ID_REQUIRED = "شناسه مورد نیاز است",
|
||||
ID_SHOULD_BE_UUID = "شناسه باید یک یو یو آی دی باشد",
|
||||
PaymentTypeQueryNotEmpty = "نوع پرداخت نمیتواند خالی باشد",
|
||||
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
|
||||
UPDATED = "با موفقیت آپدیت شد",
|
||||
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
}
|
||||
|
||||
export const enum CategoryMessage {
|
||||
TITLE_REQUIRED = "نام دسته بندی مورد نیاز است",
|
||||
TITLE_STRING = "نام دسته بندی باید یک رشته باشد",
|
||||
TITLE_LENGTH = "نام دسته بندی باید بین ۳ تا ۵۰ کاراکتر باشد",
|
||||
ICON_REQUIRED = "آیکون دسته بندی مورد نیاز است",
|
||||
ICON_SHOULD_BE_URL = "آیکون باید یک آدرس یو آر ال باشد",
|
||||
PARENT_ID_REQUIRED = "شناسه والد دسته بندی مورد نیاز است",
|
||||
PARENT_ID_SHOULD_BE_UUID = "شناسه والد باید یک یو یو آی دی باشد",
|
||||
TITLE_EXIST = "دسته بندی با این نام قبلا ثبت شده است",
|
||||
IS_ACTIVE_REQUIRED = "وضعیت دسته بندی مورد نیاز است",
|
||||
CAT_ID_SHOULD_BE_UUID = "شناسه دسته بندی باید یک یو یو آی دی باشد",
|
||||
CAT_ID_NOT_EMPTY = "شناسه دسته بندی نمیتواند خالی باشد",
|
||||
CATEGORY_NOT_EXIST = "دسته بندی مورد نظر یافت نشد",
|
||||
IS_ACTIVE_SHOULD_BE_BOOLEAN = "وضعیت دسته بندی باید یک بولین باشد",
|
||||
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
|
||||
IS_SUGGEST_SHOULD_BE_BOOLEAN = "وضعیت پیشنهادی بودن دسته بندی باید یک بولین باشد",
|
||||
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت دسته بندی باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
IS_SUGGEST_SHOULD_BE_1_0 = "وضعیت پیشنهادی بودن دسته بندی باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
CATEGORY_NOT_EXIST_OR_HAS_NO_SERVICE = "دسته بندی مورد نظر یافت نشد یا هیچ سرویسی در این دسته بندی وجود ندارد",
|
||||
ORDER_REQUIRED = "ترتیب دسته بندی مورد نیاز است",
|
||||
ORDER_SHOULD_BE_INT = "ترتیب دسته بندی باید یک عدد صحیح باشد",
|
||||
CREATION_FAILED = "ایجاد دسته بندی موفیقیت آمیز نبود",
|
||||
UPDATE_FAILED = "به روز رسانی دسته بندی موفیقیت آمیز نبود",
|
||||
}
|
||||
|
||||
export const enum ServiceMessage {
|
||||
NAME_REQUIRED = "نام سرویس مورد نیاز است",
|
||||
NAME_STRING = "نام سرویس باید یک رشته باشد",
|
||||
NAME_LENGTH = "نام سرویس باید بین ۳ تا ۱۵۰ کاراکتر باشد",
|
||||
DESCRIPTION_REQUIRED = "توضیحات سرویس مورد نیاز است",
|
||||
DESCRIPTION_STRING = "توضیحات سرویس باید یک رشته باشد",
|
||||
AUTHOR_REQUIRED = "نویسنده سرویس مورد نیاز است",
|
||||
AUTHOR_STRING = "نویسنده سرویس باید یک رشته باشد",
|
||||
CREATE_DATE_REQUIRED = "تاریخ ایجاد سرویس مورد نیاز است",
|
||||
CREATE_DATE_STRING = "تاریخ ایجاد سرویس باید یک رشته باشد",
|
||||
USER_COUNT_REQUIRED = "تعداد کاربران سرویس مورد نیاز است",
|
||||
USER_COUNT_INT = "تعداد کاربران سرویس باید یک عدد صحیح باشد",
|
||||
SERVICE_LANGUAGE_REQUIRED = "زبان سرویس مورد نیاز است",
|
||||
SOFTWARE_LANGUAGE_REQUIRED = "زبان نرم افزار سرویس مورد نیاز است",
|
||||
SOFTWARE_LANGUAGE_STRING = "زبان نرم افزار سرویس باید یک رشته باشد",
|
||||
META_DESCRIPTION_REQUIRED = "متا توضیحات سرویس مورد نیاز است",
|
||||
LINK_REQUIRED = "لینک سرویس مورد نیاز است",
|
||||
IS_DANAK_SUGGEST_REQUIRED = "آیا سرویس پیشنهادی داناک است؟",
|
||||
IS_DANAK_SUGGEST_BOOLEAN = "وضعیت سرویس پیشنهادی داناک باید یک بولین باشد",
|
||||
ICON_REQUIRED = "آیکون سرویس مورد نیاز است",
|
||||
LINK_SHOULD_BE_URL = "لینک باید یک آدرس یو آر ال باشد",
|
||||
ICON_SHOULD_BE_URL = "آیکون باید یک آدرس یو آر ال باشد",
|
||||
IMAGES_REQUIRED = "تصاویر سرویس مورد نیاز است",
|
||||
DESCRIPTION_LENGTH = "توضیحات سرویس باید بین ۱۰ تا ۵۰۰ کاراکتر باشد",
|
||||
SOFTWARE_LANGUAGE_LENGTH = "زبان نرم افزار سرویس باید بین ۳ تا ۱۵۰ کاراکتر باشد",
|
||||
AUTHOR_LENGTH = "نویسنده سرویس باید بین 3 تا 100 کاراکتر باشد",
|
||||
SERVICE_NOT_EXIST = "سرویس مورد نظر یافت نشد",
|
||||
NAME_EXIST = "سرویس با این نام قبلا ثبت شده است",
|
||||
SERVICE_NOT_FOUND_BY_ID = "سرویسی با این شناسه یافت نشد یا سرویس غیرفعال میباشد",
|
||||
SERVICE_ID_SHOULD_BE_A_UUID = "شناسه سرویس باید یک UUID باشد",
|
||||
SERVICE_ID_REQUIRED = "شناسه سرویس مورد نیاز است",
|
||||
COMMENT_REQUIRED = "نظر سرویس مورد نیاز است",
|
||||
COMMENT_STRING = "نظر سرویس باید یک رشته باشد",
|
||||
COMMENT_LENGTH = "نظر سرویس باید بین ۱ تا ۴۰۰ کاراکتر باشد",
|
||||
RATING_REQUIRED = "امتیاز سرویس مورد نیاز است",
|
||||
RATING_INT = "امتیاز سرویس باید یک عدد صحیح باشد",
|
||||
RATING_RANGE = "امتیاز سرویس باید بین ۱ تا ۵ باشد",
|
||||
TITLE_REQUIRED = "عنوان سرویس مورد نیاز است",
|
||||
TITLE_STRING = "عنوان سرویس باید یک رشته باشد",
|
||||
TITLE_LENGTH = "عنوان سرویس باید بین ۱ تا ۱۵۰ کاراکتر باشد",
|
||||
REVIEW_ADDED = "نظر شما با موفقیت ثبت شد",
|
||||
REVIEW_NOT_EXIST = "نظری برای این سرویس یافت نشد",
|
||||
REVIEW_APPROVED = "نظر با موفقیت تایید شد",
|
||||
REVIEW_REJECTED = "نظر با موفقیت رد شد",
|
||||
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت سرویس باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
IS_SUGGEST_SHOULD_BE_1_0 = "وضعیت پیشنهادی بودن سرویس باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
REVIEW_STATUS_REQUIRED = "وضعیت نظر سرویس مورد نیاز است",
|
||||
SHOW_IN_SLIDER_REQUIRED = "وضعیت نمایش در اسلایدر سرویس مورد نیاز است",
|
||||
SHOW_IN_SLIDER_BOOLEAN = "وضعیت نمایش در اسلایدر سرویس باید یک بولین باشد",
|
||||
COVER_URL_REQUIRED = "آدرس عکس کاور سرویس مورد نیاز است",
|
||||
COVER_URL_SHOULD_BE_URL = "آدرس عکس کاور سرویس باید یک آدرس یو آر ال باشد",
|
||||
SLUG_REQUIRED = "اسلاگ سرویس مورد نیاز است",
|
||||
SLUG_STRING = "اسلاگ سرویس باید یک رشته باشد",
|
||||
SLUG_LENGTH = "اسلاگ سرویس باید بین ۳ تا ۱۵۰ کاراکتر باشد",
|
||||
SERVICE_ALREADY_EXISTS_WITH_THIS_SLUG = "سرویسی با این اسلاگ قبلا ثبت شده است",
|
||||
}
|
||||
|
||||
export const enum AnnouncementMessage {
|
||||
TITLE_IS_REQUIRED = "عنوان اطلاعیه مورد نیاز است",
|
||||
TITLE_STRING = "عنوان اطلاعیه باید یک رشته باشد",
|
||||
CONTENT_IS_REQUIRED = "متن اطلاعیه مورد نیاز است",
|
||||
CONTENT_IS_STRING = "متن اطلاعیه باید رشته باشد",
|
||||
CONTENT_MUST_BE_LONGER = "متن اطلاعیه باید حداقل ۱۰ کاراکتر باشد",
|
||||
PUBLISH_AT_IS_REQUIRED = "تاریخ انتشار اطلاعیه مورد نیاز است",
|
||||
SERVICE_MUST_BE_UUID = "شناسه سرویس باید یک UUID معتبر باشد",
|
||||
GROUP_MUST_BE_UUID = "شناسه گروه کاربران باید یک UUID معتبر باشد",
|
||||
USER_MUST_BE_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
|
||||
SERVICE_MUST_BE_ID = "ایدی سرویس اجباری است",
|
||||
NOT_FOUND = "اطلاعیه ای با این ایدی یافت نشد",
|
||||
USER_IDS_MUST_BE_ARRAY = "شناسه کاربران باید یک آرایه باشد",
|
||||
IMPORTANT_IS_REQUIRED = "وضعیت اهمیت اطلاعیه مورد نیاز است",
|
||||
TITLE_LENGTH = "عنوان اطلاعیه باید بین ۵ تا ۱۰۰ کاراکتر باشد",
|
||||
PUBLISH_AT_MUST_BE_DATE = "تاریخ انتشار باید یک تاریخ معتبر باشد",
|
||||
IMPORTANT_MUST_BE_BOOLEAN = "وضعیت اهمیت اطلاعیه باید یک بولین باشد",
|
||||
SOME_SERVICES_NOT_FOUND = "بعضی از سرویس ها یافت نشدند",
|
||||
SOME_USERS_NOT_FOUND = "بعضی از کاربران یافت نشدند",
|
||||
CREATED_AND_SENT_TO_USERS_AFTER_PUBLISH = "اطلاعیه با موفقیت ایجاد شد و پس از انتشار به کاربران ارسال خواهد شد",
|
||||
CREATED_AND_SENT_TO_USERS = "اطلاعیه با موفقیت ایجاد شد و به کاربران ارسال شد",
|
||||
PUBLISH_AT_MUST_BE_FUTURE_DATE = "تاریخ انتشار باید در آینده باشد",
|
||||
UPDATED_SUCCESSFULLY = "اطلاعیه با موفقیت بهروزرسانی شد",
|
||||
DELETED_SUCCESSFULLY = "اطلاعیه با موفقیت حذف شد",
|
||||
CANNOT_DELETE_PUBLISHED = "نمیتوان اطلاعیه منتشر شده را حذف کرد",
|
||||
CANNOT_UPDATE_PUBLISHED = "نمیتوان اطلاعیه منتشر شده را ویرایش کرد",
|
||||
PUBLISH_AT_CANNOT_BE_IN_PAST = "تاریخ انتشار نمیتواند در گذشته باشد",
|
||||
USER_ANNOUNCEMENTS_DELETED = "اطلاعیههای کاربر با موفقیت حذف شدند",
|
||||
QUEUE_JOBS_DELETED = "وظایف صف با موفقیت حذف شدند",
|
||||
}
|
||||
|
||||
export const enum CriticismMessage {
|
||||
TITLE_IS_REQUIRED = "عنوان انتقادات مورد نیاز است",
|
||||
TITLE_STRING = "عنوان انتقادات باید یک رشته باشد",
|
||||
CONTENT_IS_REQUIRED = "متن انتقادات مورد نیاز است",
|
||||
CONTENT_IS_STRING = "متن انتقادات باید رشته باشد",
|
||||
NOT_FOUND = "انتقادی با این ایدی یافت نشد",
|
||||
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
|
||||
FILES_IS_REQUIRED = "فایل های انتقادات مورد نیاز است",
|
||||
FILES_SHOULD_BE_URL = "فایل های انتقادات باید یک آدرس یو آر ال باشد",
|
||||
}
|
||||
export const enum TicketMessageEnum {
|
||||
TITLE_REQUIRED = "عنوان تیکت مورد نیاز است",
|
||||
TITLE_STRING = "عنوان تیکت باید یک رشته باشد",
|
||||
TITLE_LENGTH = "عنوان تیکت باید بین ۳ تا ۱۵۰ کاراکتر باشد",
|
||||
SUBJECT_REQUIRED = "موضوع تیکت اجباری میباشد",
|
||||
SUBJECT_STRING = "موضوع تیکت باید رشته باشد",
|
||||
SUBJECT_LENGTH = "موضوع تیکت باید بین ۳ تا ۱۵۰ کاراکتر باشد",
|
||||
PRIORITY_REQUIRED = "اولویت تیکت اجباری میباشد",
|
||||
PRIORITY_INVALID = "اولویت تیکت باید یکی از مقادیر LOW، MEDIUM یا HIGH باشد",
|
||||
CATEGORY_ID_REQUIRED = "شناسه دستهبندی نمیتواند خالی باشد",
|
||||
CATEGORY_ID_SHOULD_BE_UUID = "شناسه دستهبندی باید یک UUID معتبر باشد",
|
||||
DANAK_SERVICE_ID_REQUIRED = "شناسه سرویس داناک اجباری است",
|
||||
DANAK_SERVICE_ID_SHOULD_BE_UUID = "شناسه سرویس داناک باید یک UUID معتبر باشد",
|
||||
MESSAGE_REQUIRED = "متن پیام تیکت اجباری است",
|
||||
MESSAGE_STRING = "متن پیام باید یک رشته باشد",
|
||||
MESSAGE_LENGTH = "متن پیام باید بین ۵ تا ۵۰۰ کاراکتر باشد",
|
||||
CATEGORY_EXIST = "دستهبندی با این نام قبلاً ثبت شده است",
|
||||
CATEGORY_CREATED = "دستهبندی با موفقیت ایجاد شد",
|
||||
CATEGORY_NOT_FOUND = "دستهبندی یافت نشد",
|
||||
USER_GROUP_ID_NOT_EMPTY = "شناسه گروه کاربری نمیتواند خالی باشد",
|
||||
USER_GROUP_ID_SHOULD_BE_UUID = "شناسه گروه کاربری باید یک UUID معتبر باشد",
|
||||
IS_ACTIVE_REQUIRED = "وضعیت تیکت مورد نیاز است",
|
||||
IS_ACTIVE_SHOULD_BE_BOOLEAN = "وضعیت تیکت باید یک مقدار بولی (true/false) باشد",
|
||||
CREATED = "تیکت با موفقیت ساخته شد",
|
||||
DESCRIPTION_REQUIRED = "توضیحات تیکت مورد نیاز است",
|
||||
DESCRIPTION_STRING = "توضیحات تیکت باید یک رشته باشد",
|
||||
DESCRIPTION_LENGTH = "توضیحات تیکت باید بین ۳ تا ۵۰۰ کاراکتر باشد",
|
||||
CONTENT_REQUIRED = "محتوای پیام تیکت مورد نیاز است",
|
||||
ATTACHMENT_REQUIRED = "آدرس فایل پیوست تیکت مورد نیاز است",
|
||||
ATTACHMENT_SHOULD_BE_URL = "آدرس فایل پیوست تیکت باید یوآرال باشد",
|
||||
CONTENT_STRING = "محتوای پیام تیکت باید یک رشته باشد",
|
||||
MESSAGE_CREATED = "پیام تیکت با موفقیت ایجاد شد",
|
||||
TICKET_NOT_FOUND = "تیکت مورد نظر یافت نشد",
|
||||
CLOSED = "تیکت با موفقیت بسته شد",
|
||||
TICKET_CLOSED = "تیکت قبلا بسته شده است",
|
||||
ERROR_IN_TICKET_MSG_CREATION = "خطا در ایجاد پیام تیکت",
|
||||
SERVICE_NOT_FOUND = "سرویس مورد نظر یافت نشد",
|
||||
TICKET_NOT_ASSIGNED = "تیکت به کارشناسی اختصاص داده نشده است",
|
||||
TICKET_NOT_ASSIGNED_TO_USER = "تیکت به شما اختصاص داده نشده است",
|
||||
REFERRED = "REFERRED",
|
||||
NO_ACTIVE_SUPPORT_PLAN = "شما پلن پشتیبانی فعال ندارید",
|
||||
TICKET_LIMIT_NOT_FOUND = "محدودیت تعداد تیکت در پلن پشتیبانی شما یافت نشد",
|
||||
INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است",
|
||||
TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیدهاید",
|
||||
OPENED = "تیکت با موفقیت باز شد",
|
||||
}
|
||||
|
||||
export const enum WalletMessage {
|
||||
WALLET_NOT_FOUND = "کیف پول یافت نشد",
|
||||
AMOUNT_MUST_BE_INTEGER = "مبلغ شارژ باید یک عدد صحیح باشد",
|
||||
AMOUNT_REQUIRED = "مبلغ شارژ مورد نیاز است",
|
||||
DEPOSIT_TYPE_REQUIRED = "نوع شارژ مورد نیاز است",
|
||||
AMOUNT_MINIMUM = "حداقل مبلغ شارژ ۱۰۰,۰۰۰ تومان است",
|
||||
GATEWAY_REQUIRED = "درگاه پرداخت مورد نیاز است",
|
||||
RECEIPT_URL_SHOULD_BE_URL = "آدرس فیش باید یک آدرس یو آر ال باشد",
|
||||
RECEIPT_URL_REQUIRED = "آدرس فیش مورد نیاز است",
|
||||
BANK_ACCOUNT_ID_REQUIRED = "شناسه حساب بانکی مورد نیاز است",
|
||||
BANK_ACCOUNT_ID_SHOULD_BE_UUID = "شناسه حساب بانکی باید یک UUID معتبر باشد",
|
||||
ERROR_IN_CHARGE_WALLET = "خطا در شارژ کیف پول",
|
||||
DEPOSIT_WALLET_IPG = "شارژ کیف پول از طریق درگاه",
|
||||
TRANSFER_METHOD_REQUIRED = "روش انتفال اجباری است",
|
||||
GATEWAY_ID_SHOULD_BE_UUID = "آیدی درگاه پرداخت باید UUID باشد",
|
||||
DEPOSIT_WALLET_TRANSFER = "شارژ کیف پول از طریق انتقال بانکی",
|
||||
INSUFFICIENT_BALANCE = "موجودی کیف پول کافی نیست",
|
||||
SUBSCRIPTION_WALLET_TRANSFER = "پرداخت اشتراک از طریق کیف پول",
|
||||
SUPPORT_PLAN_WALLET_TRANSFER = "پرداخت پلن پشتیبانی از طریق کیف پول",
|
||||
INVOICE_WALLET_TRANSFER = "پرداخت صورت حساب از طریق کیف پول",
|
||||
TRANSACTION_NOT_FOUND = "تراکنش پیدا نشد",
|
||||
REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول",
|
||||
}
|
||||
|
||||
export const enum PaymentMessage {
|
||||
PAYMENT_GATEWAY_NOT_FOUND = "درگاه پرداخت یافت نشد",
|
||||
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
|
||||
ERROR_IN_VERIFY_PAYMENT = "خطا در تایید پرداخت",
|
||||
PAYMENT_NOT_FOUND_WITH_REF = "پرداختی با این شناسه یافت نشد",
|
||||
CARD_NUMBER_REQUIRED = "شماره کارت الزامی است",
|
||||
CARD_NUMBER_NUMBER_STRING = "شماره کارت باید شامل اعداد باشد",
|
||||
CARD_NUMBER_LENGTH = "شماره کارت باید ۱۶ رقم باشد",
|
||||
IBAN_REQUIRED = "شماره شبا الزامی است",
|
||||
IBAN_NUMBER_STRING = "شماره شبا باید شامل اعداد باشد",
|
||||
IBAN_INVALID = "شماره شبا معتبر نیست",
|
||||
BANK_NAME_REQUIRED = "نام بانک الزامی است",
|
||||
BANK_NAME_STRING = "نام بانک باید شامل حروف باشد",
|
||||
BANK_NAME_LENGTH = "نام بانک باید بین ۲ تا ۱۰۰ کاراکتر باشد",
|
||||
ACCOUNT_OWNER_NAME_REQUIRED = "نام صاحب حساب الزامی است",
|
||||
ACCOUNT_OWNER_NAME_STRING = "نام صاحب حساب باید شامل حروف باشد",
|
||||
ACCOUNT_OWNER_NAME_LENGTH = "نام صاحب حساب باید بین ۳ تا ۱۵۰ کاراکتر باشد",
|
||||
IS_ACTIVE_REQUIRED = "وضعیت فعال بودن حساب الزامی است",
|
||||
IS_ACTIVE_BOOLEAN = "وضعیت فعال بودن حساب باید true یا false باشد",
|
||||
CARD_NUMBER_EXIST = "شماره کارت تکراری میباشد",
|
||||
IBAN_EXIST = "شماره شبا تکراری میباشد",
|
||||
BANK_ACCOUNT_NOT_FOUND = "حساب بانکی پیدا نشد",
|
||||
DEPOSIT_CREATED = "درخواست واریز ثبت شد",
|
||||
VALIDATED_BEFORE = "این پرداخت قبلا تایید شده است",
|
||||
DEPOSIT_NOT_FOUND = "درخواست واریز پیدا نشد",
|
||||
PAYMENT_NOT_FOUND = "پرداخت پیدا نشد",
|
||||
DEPOSIT_APPROVED = "درخواست واریز تایید شد",
|
||||
DEPOSIT_REJECTED = "درخواست واریز رد شد",
|
||||
REJECT_COMMENT_REQUIRED = "توضیحات رد درخواست واریز مورد نیاز است",
|
||||
REJECT_COMMENT_STRING = "توضیحات رد درخواست واریز باید یک رشته باشد",
|
||||
REJECT_COMMENT_LENGTH = "توضیحات رد درخواست واریز باید بین ۳ تا ۲۵۰ کاراکتر باشد",
|
||||
TRANSFER_METHOD_NOT_ALLOWED = "روش انتقال انتخابی مجاز نیست",
|
||||
DEPOSIT_ALREADY_APPROVED = "درخواست واریز قبلا تایید شده است",
|
||||
DEPOSIT_ALREADY_REJECTED = "درخواسن واریز قبلا رد شده است",
|
||||
}
|
||||
|
||||
export const enum SettingMessageEnum {
|
||||
NOTIF_NOT_FOUND = "تنظیمات نوتیف پیدا نشد",
|
||||
NOT_FOUND = "تنظیمات پیدا نشد",
|
||||
SMS_MUST_BE_BOOLEAN = "تنظیمات اسمس باید از نوع BOOLEAN باشد",
|
||||
EMAIL_MUST_BE_BOOLEAN = "تنظیمات ایمیل باید از نوع BOOLEAN باشد",
|
||||
}
|
||||
|
||||
export const enum ContactUsMessage {
|
||||
FULLNAME_IS_REQUIRED = "نام کامل مورد نیاز است",
|
||||
FULLNAME_STRING = "نام کامل باید یک رشته باشد",
|
||||
EMAIL_IS_REQUIRED = "ایمیل مورد نیاز است",
|
||||
INVALID_EMAIL_FORMAT = "فرمت ایمیل وارد شده صحیح نیست",
|
||||
CONTENT_IS_REQUIRED = "محتوا مورد نیاز است",
|
||||
CONTENT_STRING = "محتوا باید یک رشته باشد",
|
||||
TITLE_IS_REQUIRED = "عنوان مورد نیاز است",
|
||||
TITLE_STRING = "عنوان باید یک رشته باشد",
|
||||
NOT_FOUND = "پیامی با این ایدی یافت نشد",
|
||||
BUSINESS_NAME_IS_REQUIRED = "نام شرکت مورد نیاز است",
|
||||
BUSINESS_NAME_STRING = "نام شرکت باید یک رشته باشد",
|
||||
BUSINESS_NAME_LENGTH = "نام شرکت باید بین ۲ تا ۱۰۰ کاراکتر باشد",
|
||||
}
|
||||
|
||||
export const enum NotificationMessage {
|
||||
NOT_FOUNT = "اعلان مورد نظر یافت نشد",
|
||||
TITLE_IS_REQUIRED = "عنوان الزامی است",
|
||||
TITLE_STRING = "عنوان باید یک رشته متنی باشد",
|
||||
MESSAGE_IS_REQUIRED = "پیام الزامی است",
|
||||
MESSAGE_STRING = "پیام باید یک رشته متنی باشد",
|
||||
USERID_IS_REQUIRED = "ایدی کاربر الزامی است",
|
||||
USERID_IS_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
|
||||
USERID_IS_STRING = "شناسه کاربر باید یک رشته متنی باشد",
|
||||
LOGIN = "لاگین",
|
||||
LOGIN_MESSAGE = "یک لاگین در تاریخ [loginDate] به حساب کاربری شما صورت گرفت",
|
||||
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی غیرمجاز است",
|
||||
INVOICE_CREATION = "صدور صورت حساب",
|
||||
INVOICE_CREATION_MESSAGE = "یک صورت حساب به شماره [id] جدید برای شما صادر شده است",
|
||||
ANNOUNCEMENT = "اطلاعیه",
|
||||
ANNOUNCEMENT_MESSAGE = "یک اطلاعیه جدید برای شما ارسال شده است",
|
||||
WALLET_CHARGE = "شارژ کیف پول",
|
||||
WALLET_CHARGE_MESSAGE = "کیف پول شما به مبلغ [amount] تومان شارژ شد",
|
||||
WALLET_DEDUCTION = "کسر از کیف پول",
|
||||
WALLET_DEDUCTION_MESSAGE = "مبلغ [amount] تومان از کیف پول شما کسر شد",
|
||||
BILL_INVOICE_REMINDER = "یادآوری پرداخت صورت حساب",
|
||||
BILL_INVOICE_REMINDER_MESSAGE = "صورت حساب شماره [invoiceNumber] هنوز پرداخت نشده است",
|
||||
BILL_INVOICE = "صورت حساب پرداختی",
|
||||
BILL_INVOICE_MESSAGE = "صورت حساب شماره [invoiceNumber] با موفقیت پرداخت شد",
|
||||
APPROVED_INVOICE_MESSAGE = "صورت حساب شماره [invoiceNumber] تایید شد",
|
||||
APPROVED_INVOICE = "تایید صورت حساب",
|
||||
INVOICE_OVERDUE = "صورت حساب معوق",
|
||||
INVOICE_OVERDUE_MESSAGE = "صورت حساب شماره [invoiceNumber] معوق شده و دارای مبلغ [lateFee] جریمه تاخیر است",
|
||||
CREATE_SERVICE = "ایجاد سرویس",
|
||||
CREATE_SERVICE_MESSAGE = "سرویس جدیدی با عنوان [serviceName] ایجاد شد",
|
||||
UNBLOCK_SERVICE = "فعالسازی سرویس",
|
||||
UNBLOCK_SERVICE_MESSAGE = "سرویس [serviceName] مجددا فعال شده است",
|
||||
BLOCK_SERVICE = "غیرفعالسازی سرویس",
|
||||
BLOCK_SERVICE_MESSAGE = "سرویس [serviceName] به دلیل عدم پرداخت صورتحساب غیرفعال شده است",
|
||||
ANSWER_TICKET = "پاسخ تیکت",
|
||||
ANSWER_TICKET_MESSAGE = "پاسخ جدیدی برای تیکت [ticketId] دریافت کردید",
|
||||
CREATE_TICKET = "ایجاد تیکت",
|
||||
CREATE_TICKET_MESSAGE = "تیکت شما با شماره [ticketId] ثبت شد",
|
||||
ASSIGN_TICKET_MESSAGE = "یک تیکت به شماره [ticketId] به شما اختصاص یافت",
|
||||
ASSIGN_TICKET = "اختصاص تیکت",
|
||||
RECURRING_INVOICE_MESSAGE = "صورت حساب دورهای شماره [invoiceNumber] صادر شده است، لطفا نسبت به تایید یا ویرایش آن اقدام کنید",
|
||||
RECURRING_INVOICE = "صورت حساب دورهای",
|
||||
PAYMENT_REMINDER = "یادآوری پرداخت",
|
||||
PAYMENT_REMINDER_MESSAGE = "لطفا مبلغ [amount] تومان را پرداخت کنید",
|
||||
PAYMENT_CANCELLATION = "لغو پرداخت",
|
||||
PAYMENT_CANCELLATION_MESSAGE = "پرداخت مبلغ [amount] تومان لغو شد",
|
||||
// Admin notification messages
|
||||
NEW_BLOG_COMMENT = "نظر جدید در بلاگ",
|
||||
NEW_BLOG_COMMENT_MESSAGE = "یک نظر جدید در بلاگ [blogTitle] ثبت شده است",
|
||||
NEW_SERVICE_REVIEW = "نقد و بررسی جدید سرویس",
|
||||
NEW_SERVICE_REVIEW_MESSAGE = "یک نقد و بررسی جدید برای سرویس [serviceName] ثبت شده است",
|
||||
NEW_CUSTOMER = "مشتری جدید",
|
||||
NEW_CUSTOMER_MESSAGE = "یک مشتری جدید با نام [fullName] ثبت نام کرده است",
|
||||
NEW_SUBSCRIPTION = "اشتراک جدید",
|
||||
NEW_SUBSCRIPTION_MESSAGE = "یک اشتراک جدید برای پلن [serviceName] برای [fullName] ثبت شد",
|
||||
NEW_TICKET = "تیکت جدید",
|
||||
NEW_TICKET_MESSAGE = "یک تیکت جدید با موضوع [ticketSubject] ثبت شده است",
|
||||
NEW_CRITICISM = "انتقاد جدید",
|
||||
NEW_CRITICISM_MESSAGE = "یک انتقاد جدید با موضوع [title] ثبت شده است",
|
||||
}
|
||||
|
||||
export const enum InvoiceMessage {
|
||||
COUNT_REQUIRED = "تعداد محصول مورد نیاز است",
|
||||
COUNT_MUST_BE_A_INT = "تعداد محصول باید یک عدد صحیح باشد",
|
||||
UNIT_PRICE_REQUIRED = "قیمت واحد محصول مورد نیاز است",
|
||||
UNIT_PRICE_MUST_BE_A_INT = "قیمت واحد محصول باید یک عدد صحیح باشد",
|
||||
DISCOUNT_REQUIRED = "تخفیف محصول مورد نیاز است",
|
||||
DISCOUNT_MUST_BE_A_INT = "تخفیف محصول باید یک عدد صحیح باشد",
|
||||
NAME_REQUIRED = "نام محصول مورد نیاز است",
|
||||
NAME_MUST_BE_A_STRING = "نام محصول باید یک رشته باشد",
|
||||
NAME_LENGTH = "نام محصول باید بین ۳ تا ۱۰۰ کاراکتر باشد",
|
||||
USER_ID_REQUIRED = "شناسه کاربر مورد نیاز است",
|
||||
USER_ID_SHOULD_BE_A_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
|
||||
CREATED = "صورت حساب با موفقیت ایجاد شد",
|
||||
SEARCH_QUERY_MUST_BE_A_STRING = "رشته جستجو باید یک رشته باشد",
|
||||
NOT_FOUND_BY_ID = "صورت حسابی با این شناسه یافت نشد",
|
||||
APPROVED = "صورت حساب با موفقیت تایید شد",
|
||||
ALREADY_APPROVED = "صورت حساب قبلا تایید شده است",
|
||||
INVOICE_CAN_NOT_APPROVED = "صورت حساب قابل تایید نیست",
|
||||
INVOICE_ALREADY_PAID = "صورت حساب قبلا پرداخت شده است",
|
||||
INVOICE_IS_OVERDUE = "صورت حساب منقضی شده است",
|
||||
INVOICE_PAID = "صورت حساب با موفقیت پرداخت شد",
|
||||
DISCOUNT_APPLIED = "تخفیف با موفقیت اعمال شد",
|
||||
DISCOUNT_CANCELED = "تخفیف با موفقیت لغو شد",
|
||||
INVOICE_IS_REJECTED = "صورت حساب قبلا رد شده است",
|
||||
INVOICE_CAN_NOT_PAID = "صورت حساب قابل پرداخت نیست",
|
||||
INVALID_DATE = "تاریخ وارد شده معتبر نیست",
|
||||
IS_RECURRING_MUST_BE_A_BOOLEAN = "وضعیت تکرار باید یک بولین باشد",
|
||||
RECURRING_PERIOD_REQUIRED = "دوره تکرار مورد نیاز است",
|
||||
INVOICE_CAN_NOT_UPDATE = "صورت حساب قابل ویرایش نیست",
|
||||
INVOICE_UPDATED = "صورت حساب با موفقیت به روز رسانی شد",
|
||||
DISCOUNT_CODE_REQUIRED = "کد تخفیف الزامی است",
|
||||
DISCOUNT_CODE_MUST_BE_A_STRING = "کد تخفیف باید یک رشته باشد",
|
||||
DISCOUNT_CODE_LENGTH = "طول کد تخفیف باید بین ۱ تا ۱۰۰ کاراکتر باشد",
|
||||
ALREADY_HAS_DISCOUNT = "این صورت حساب قبلا دارای تخفیف است",
|
||||
NO_DISCOUNT = "این صورت حساب دارای تخفیف نیست",
|
||||
NOT_AUTHORIZED = "شما مجاز به انجام این عملیات نیستید",
|
||||
INVALID_DISCOUNT_CODE = "کد تخفیف نامعتبر است",
|
||||
ORIGINAL_PRICE_NOT_FOUND = "قیمت اصلی صورت حساب یافت نشد",
|
||||
NOT_FOUND_BY_ID_OR_NOT_BELONG_TO_USER = "صورت حساب یافت نشد یا متعلق به کاربر نمی باشد",
|
||||
DISCOUNT_ALREADY_APPLIED = "ت`خفیف قبلا برای این صورت حساب اعمال شده است",
|
||||
DISCOUNT_EXPIRED = "تخفیف منقضی شده است",
|
||||
MAX_RECURRING_CYCLES_MUST_BE_POSITIVE = "تعداد تکرار باید بزرگتر از ۰ باشد",
|
||||
ITEMS_REQUIRED = "آیتمها مورد نیاز است",
|
||||
UNIT_PRICE_MUST_BE_POSITIVE = "قیمت واحد باید مثبت باشد",
|
||||
COUNT_MUST_BE_POSITIVE = "تعداد باید مثبت باشد",
|
||||
DISCOUNT_MUST_BE_BETWEEN_0_AND_100 = "تخفیف باید بین ۰ تا ۱۰۰ باشد",
|
||||
TOTAL_PRICE_MUST_BE_POSITIVE = "قیمت کل باید مثبت باشد",
|
||||
DISCOUNT_NOT_FOR_THIS_USER = "این تخفیف برای شما معتبر نیست",
|
||||
}
|
||||
|
||||
export const enum EmailMessage {
|
||||
EMAIL_VERIFICATION = "تایید ایمیل",
|
||||
EMAIL_SENDING_FAILED = "ارسال ایمیل با خطا مواجه شد",
|
||||
LOGIN = "ورود به سیستم",
|
||||
INVOICE = "فاکتور",
|
||||
WALLET = "اعلان کیف پول",
|
||||
TICKET = "تیکت پشتیبانی",
|
||||
ANNOUNCEMENT = "اعلان",
|
||||
PAYMENT_REMINDER = "یادآوری پرداخت",
|
||||
PAYMENT_CANCELLATION = "لغو پرداخت",
|
||||
}
|
||||
|
||||
export const enum AdminMessage {
|
||||
ROLE_REQUIRED = "نقش مورد نیاز است",
|
||||
ROLE_ID_SHOULD_BE_UUID = "شناسه نقش باید یک UUID معتبر باشد",
|
||||
PERMISSIONS_REQUIRED = "دسترسیها مورد نیاز است",
|
||||
PERMISSIONS_ID_SHOULD_BE_UUID = "شناسه دسترسی باید یک UUID معتبر باشد",
|
||||
PROFILE_PIC_URL = "آدرس تصویر پروفایل باید یک URL معتبر باشد",
|
||||
PROFILE_PIC_REQUIRED = "آدرس تصویر پروفایل مورد نیاز است",
|
||||
PERMISSIONS_NOT_FOUND = "دسترسیهای مورد نظر یافت نشد",
|
||||
ADMIN_CREATED = "مدیر با موفقیت ایجاد شد",
|
||||
PASSWORD_NOT_MATCH = "رمز عبور و تکرار آن باید یکسان باشد",
|
||||
ROLE_NAME_REQUIRED = "نام نقش مورد نیاز است",
|
||||
ROLE_NAME_STRING = "نام نقش باید یک رشته باشد",
|
||||
ROLE_NAME_LENGTH = "نام نقش باید بین ۳ تا ۱۵۰ کاراکتر باشد",
|
||||
ROLE_EXIST = "نقش با این نام قبلا ثبت شده است",
|
||||
ROLE_CREATED = "نقش با موفقیت ایجاد شد",
|
||||
ADMIN_UPDATED = "مدیر با موفقیت به روز رسانی شد",
|
||||
ADMIN_NOT_FOUND = "مدیری با این شناسه یافت نشد",
|
||||
ADMIN_DELETED = "مدیر با موفقیت حذف شد",
|
||||
NOT_ALLOWED = "شما مجوز دسترسی به این عملیات را ندارید",
|
||||
}
|
||||
|
||||
export const enum SmsMessage {
|
||||
SEND_SMS_ERROR = "خطا در ارسال پیامک",
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface ErrorResponse {
|
||||
statusCode: number;
|
||||
success: boolean;
|
||||
error: {
|
||||
message: string | string[];
|
||||
timestamp?: string;
|
||||
path?: string;
|
||||
};
|
||||
}
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
export interface IPageFormat {
|
||||
page: number;
|
||||
limit: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
prevPage: string | boolean;
|
||||
nextPage: string | boolean;
|
||||
}
|
||||
|
||||
export interface PaginationQuery {
|
||||
page?: string;
|
||||
limit?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse {
|
||||
paginate?: boolean;
|
||||
count?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ValueProvider } from "@nestjs/common";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
export const MIKRO_ORM_QUERY_LOGGER = "MikroOrmQueryLogger";
|
||||
|
||||
export const MikroOrmQueryLogger: ValueProvider = {
|
||||
provide: MIKRO_ORM_QUERY_LOGGER,
|
||||
useValue: new Logger("mikro-orm"),
|
||||
};
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
import { OnWorkerEvent, WorkerHost } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bullmq";
|
||||
|
||||
export abstract class WorkerProcessor extends WorkerHost {
|
||||
protected readonly logger = new Logger(WorkerProcessor.name);
|
||||
|
||||
@OnWorkerEvent("completed")
|
||||
onCompleted(job: Job) {
|
||||
const { id, name, queueName, finishedOn, returnvalue } = job;
|
||||
const completionTime = finishedOn ? new Date(finishedOn).toISOString() : "";
|
||||
this.logger.log(`Job id: ${id}, name: ${name} completed in queue ${queueName} on ${completionTime}. Result: ${returnvalue}`);
|
||||
}
|
||||
|
||||
@OnWorkerEvent("progress")
|
||||
onProgress(job: Job) {
|
||||
const { id, name, progress } = job;
|
||||
this.logger.log(`Job id: ${id}, name: ${name} completes ${progress}%`);
|
||||
}
|
||||
|
||||
@OnWorkerEvent("failed")
|
||||
onFailed(job: Job) {
|
||||
const { id, name, queueName, failedReason } = job;
|
||||
this.logger.error(`Job id: ${id}, name: ${name} failed in queue ${queueName}. Failed reason: ${failedReason}`);
|
||||
}
|
||||
|
||||
@OnWorkerEvent("active")
|
||||
onActive(job: Job) {
|
||||
const { id, name, queueName, timestamp } = job;
|
||||
const startTime = timestamp ? new Date(timestamp).toISOString() : "";
|
||||
this.logger.log(`Job id: ${id}, name: ${name} starts in queue ${queueName} on ${startTime}.`);
|
||||
}
|
||||
}
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
import { SharedBullAsyncConfiguration } from "@nestjs/bullmq";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function bullMqConfig(): SharedBullAsyncConfiguration {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
connection: {
|
||||
url: configService.getOrThrow<string>("REDIS_URI"),
|
||||
},
|
||||
prefix: "dzone",
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: false,
|
||||
removeOnFail: false,
|
||||
attempts: 5,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import { CacheModuleAsyncOptions } from "@nestjs/cache-manager";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function cacheConfig(): CacheModuleAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
isGlobal: true,
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
ttl: configService.getOrThrow<string>("CACHE_TTL"),
|
||||
stores: [new KeyvRedis(configService.getOrThrow<string>("REDIS_URI"), { namespace: "dzone:" })],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
import { HttpModuleAsyncOptions } from "@nestjs/axios";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function httpConfig(): HttpModuleAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
timeout: configService.getOrThrow<number>("AXIOS_TIMEOUT"),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
global: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtModuleAsyncOptions } from "@nestjs/jwt";
|
||||
|
||||
export function jwtConfig(): JwtModuleAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
return {
|
||||
secret: configService.getOrThrow<string>("JWT_SECRET"),
|
||||
signOptions: {
|
||||
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { HandlebarsAdapter } from "@nestjs-modules/mailer/dist/adapters/handlebars.adapter";
|
||||
import { MailerAsyncOptions } from "@nestjs-modules/mailer/dist/interfaces/mailer-async-options.interface";
|
||||
|
||||
export function mailerConfig(): MailerAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
transport: {
|
||||
host: configService.getOrThrow<string>("SMTP_HOST"),
|
||||
port: configService.getOrThrow<number>("SMTP_PORT"),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: configService.getOrThrow<string>("SMTP_USER"),
|
||||
pass: configService.getOrThrow<string>("SMTP_PASS"),
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: configService.getOrThrow<string>("MAIL_FROM"),
|
||||
},
|
||||
|
||||
template: {
|
||||
dir: process.cwd() + "/src/templates/email",
|
||||
adapter: new HandlebarsAdapter(),
|
||||
options: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
import { MikroOrmModuleAsyncOptions } from "@mikro-orm/nestjs";
|
||||
import { PostgreSqlDriver } from "@mikro-orm/postgresql";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { MIKRO_ORM_QUERY_LOGGER, MikroOrmQueryLogger } from "../common/providers/mikro-orm-logger";
|
||||
|
||||
export const databaseConfig: MikroOrmModuleAsyncOptions = {
|
||||
providers: [MikroOrmQueryLogger],
|
||||
inject: [ConfigService, MIKRO_ORM_QUERY_LOGGER],
|
||||
driver: PostgreSqlDriver,
|
||||
useFactory: (configService: ConfigService, logger: Logger) => ({
|
||||
driver: PostgreSqlDriver,
|
||||
autoLoadEntities: true,
|
||||
dbName: configService.getOrThrow<string>("DB_NAME"),
|
||||
user: configService.getOrThrow<string>("DB_USER"),
|
||||
password: configService.getOrThrow<string>("DB_PASS"),
|
||||
host: configService.getOrThrow<string>("DB_HOST"),
|
||||
port: configService.getOrThrow<number>("DB_PORT"),
|
||||
debug: configService.getOrThrow<string>("NODE_ENV") !== "production",
|
||||
ensureDatabase: { forceCheck: true, create: true, schema: "update" },
|
||||
// entities: ["./dist/**/*.entity.js"],
|
||||
// entitiesTs: ["./src/**/*.entity.ts"],
|
||||
forceUtcTimezone: true,
|
||||
timezone: "UTC",
|
||||
strict: true,
|
||||
validate: true,
|
||||
pool: {
|
||||
min: 2, // Minimum number of connections in pool
|
||||
max: 10, // Maximum number of connections in pool
|
||||
idleTimeoutMillis: 30000, // 30 seconds before idle connection is closed
|
||||
acquireTimeoutMillis: 10000, // 10 seconds to acquire a connection before throwing error
|
||||
reapIntervalMillis: 1000, // How often to check for idle connections
|
||||
createTimeoutMillis: 3000, // How long to wait when creating a new connection
|
||||
destroyTimeoutMillis: 5000, // How long to wait when destroying a connection
|
||||
},
|
||||
logger: (message) => logger.debug(message),
|
||||
schemaGenerator: {
|
||||
createForeignKey: true,
|
||||
disableForeignKeys: false,
|
||||
createIndex: true,
|
||||
},
|
||||
migrations: {
|
||||
path: "./database/migrations",
|
||||
pathTs: "./database/migrations",
|
||||
tableName: "migrations",
|
||||
transactional: true,
|
||||
allOrNothing: true,
|
||||
dropTables: false,
|
||||
safe: true,
|
||||
emit: "ts",
|
||||
},
|
||||
}),
|
||||
};
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ThrottlerAsyncOptions } from "@nestjs/throttler";
|
||||
|
||||
import { AuthMessage } from "../common/enums/message.enum";
|
||||
|
||||
export function rateLimitConfig(): ThrottlerAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
throttlers: [
|
||||
{
|
||||
ttl: configService.getOrThrow("THROTTLE_TTL"),
|
||||
limit: configService.getOrThrow("THROTTLE_LIMIT"),
|
||||
},
|
||||
],
|
||||
errorMessage: AuthMessage.TOO_MANY_REQUESTS,
|
||||
storage: new ThrottlerStorageRedisService(configService.getOrThrow("REDIS_URI")),
|
||||
}),
|
||||
};
|
||||
}
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function S3Configs() {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
return {
|
||||
accessKeyId: configService.getOrThrow<string>("BUCKET_ACCESS_KEY"),
|
||||
secretAccessKey: configService.getOrThrow<string>("BUCKET_SECRET_KEY"),
|
||||
endpoint: configService.getOrThrow<string>("BUCKET_URL"),
|
||||
region: configService.getOrThrow<string>("BUCKET_REGION"),
|
||||
bucket: configService.getOrThrow<string>("BUCKET_NAME"),
|
||||
url: configService.getOrThrow<string>("BUCKET_UPLOAD_URL"),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface IS3Configs {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
url: string;
|
||||
}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function smsConfigs() {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory(configService: ConfigService) {
|
||||
return {
|
||||
API_URL: configService.getOrThrow<string>("SMS_API_URL"),
|
||||
API_KEY: configService.getOrThrow<string>("SMS_API_KEY"),
|
||||
SMS_PATTERN_OTP: configService.getOrThrow<string>("SMS_PATTERN_OTP"),
|
||||
SMS_PATTERN_INVOICE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE"),
|
||||
SMS_PATTERN_LOGIN: configService.getOrThrow<string>("SMS_PATTERN_LOGIN"),
|
||||
SMS_PATTERN_INVOICE_CREATED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_CREATED"),
|
||||
SMS_PATTERN_ANNOUNCEMENT: configService.getOrThrow<string>("SMS_PATTERN_ANNOUNCEMENT"),
|
||||
SMS_PATTERN_TICKET_CREATED: configService.getOrThrow<string>("SMS_PATTERN_TICKET_CREATED"),
|
||||
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: configService.getOrThrow<string>("SMS_PATTERN_TICKET_ASSIGNED_ADMIN"),
|
||||
SMS_PATTERN_INVOICE_APPROVED: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_APPROVED"),
|
||||
SMS_PATTERN_INVOICE_PAID: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_PAID"),
|
||||
SMS_PATTERN_RECURRING_INVOICE_DRAFT: configService.getOrThrow<string>("SMS_PATTERN_RECURRING_INVOICE_DRAFT"),
|
||||
SMS_PATTERN_SUBSCRIPTION_CANCELLED: configService.getOrThrow<string>("SMS_PATTERN_SUBSCRIPTION_CANCELLED"),
|
||||
SMS_PATTERN_INVOICE_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_REMINDER"),
|
||||
SMS_PATTERN_INVOICE_OVERDUE: configService.getOrThrow<string>("SMS_PATTERN_INVOICE_OVERDUE"),
|
||||
SMS_PATTERN_PAYMENT_REMINDER: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_REMINDER"),
|
||||
SMS_PATTERN_PAYMENT_CANCELLATION: configService.getOrThrow<string>("SMS_PATTERN_PAYMENT_CANCELLATION"),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISmsConfigs {
|
||||
API_URL: string;
|
||||
API_KEY: string;
|
||||
SMS_PATTERN_OTP: string;
|
||||
SMS_PATTERN_INVOICE: string;
|
||||
SMS_PATTERN_LOGIN: string;
|
||||
SMS_PATTERN_INVOICE_CREATED: string;
|
||||
SMS_PATTERN_ANNOUNCEMENT: string;
|
||||
SMS_PATTERN_TICKET_CREATED: string;
|
||||
SMS_PATTERN_TICKET_ANSWERED: string;
|
||||
SMS_PATTERN_TICKET_ASSIGNED_ADMIN: string;
|
||||
SMS_PATTERN_INVOICE_APPROVED: string;
|
||||
SMS_PATTERN_INVOICE_PAID: string;
|
||||
SMS_PATTERN_RECURRING_INVOICE_DRAFT: string;
|
||||
SMS_PATTERN_SUBSCRIPTION_CANCELLED: string;
|
||||
SMS_PATTERN_INVOICE_REMINDER: string;
|
||||
SMS_PATTERN_INVOICE_OVERDUE: string;
|
||||
SMS_PATTERN_PAYMENT_REMINDER: string;
|
||||
SMS_PATTERN_PAYMENT_CANCELLATION: string;
|
||||
}
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
import { NestFastifyApplication } from "@nestjs/platform-fastify";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
|
||||
export function getSwaggerDocument(app: NestFastifyApplication) {
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle("The Danak dsc api document")
|
||||
.setDescription("The Danak dsc API description")
|
||||
.addBearerAuth(
|
||||
{
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
name: "authorization",
|
||||
in: "header",
|
||||
},
|
||||
"authorization",
|
||||
)
|
||||
|
||||
.setVersion("1.0.0")
|
||||
.build();
|
||||
|
||||
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
|
||||
SwaggerModule.setup("api-docs", app, swaggerDocument);
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
export function zarinpalConfig() {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
merchantId: configService.getOrThrow<string>("ZARINPAL_MERCHANT_ID"),
|
||||
gatewayApiUrl: configService.getOrThrow<string>("ZARINPAL_API_URL"),
|
||||
callBackUrl: configService.getOrThrow<string>("CALLBACK_URL"),
|
||||
ipgType: configService.getOrThrow<string>("IPG_TYPE"),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export interface IZarinpalConfig {
|
||||
merchantId: string;
|
||||
gatewayApiUrl: string;
|
||||
callBackUrl: string;
|
||||
ipgType: string;
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { ErrorResponse } from "../../common/interfaces/IError-response";
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const reply = ctx.getResponse<FastifyReply>();
|
||||
const request = ctx.getRequest<FastifyRequest>();
|
||||
const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const exceptionResponse = exception.getResponse();
|
||||
const message = this.extractMessage(exceptionResponse);
|
||||
|
||||
const response: ErrorResponse = {
|
||||
statusCode: status,
|
||||
success: false,
|
||||
error: {
|
||||
message,
|
||||
path: request.url,
|
||||
},
|
||||
};
|
||||
|
||||
reply.status(status).send(response);
|
||||
}
|
||||
|
||||
private extractMessage(response: string | Record<string, any>): string[] {
|
||||
if (typeof response === "string") {
|
||||
return [response];
|
||||
}
|
||||
|
||||
if (typeof response === "object" && response !== null) {
|
||||
if ("message" in response) {
|
||||
const message = response.message;
|
||||
return Array.isArray(message) ? message : [message];
|
||||
}
|
||||
|
||||
if ("error" in response) {
|
||||
return [response.error];
|
||||
}
|
||||
}
|
||||
|
||||
return ["An unexpected error occurred"];
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
|
||||
import { FastifyRequest } from "fastify";
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
import { IPageFormat, PaginatedResponse, PaginationQuery } from "../../common/interfaces/IPagination";
|
||||
|
||||
@Injectable()
|
||||
export class PaginationInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(PaginationInterceptor.name);
|
||||
private readonly DEFAULT_PAGE = 1;
|
||||
private readonly DEFAULT_LIMIT = 10;
|
||||
private readonly MAX_LIMIT = 100;
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
const { page, limit } = this.extractPaginationParams(request.query as PaginationQuery);
|
||||
const { className, handlerName } = this.getContextInfo(context);
|
||||
|
||||
return next.handle().pipe(
|
||||
map((data: PaginatedResponse) => {
|
||||
if (!this.isPaginatedResponse(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
this.logger.log(`Paginating response from ${className}.${handlerName}`);
|
||||
const { count, paginate, ...response } = data;
|
||||
const pager = this.formatPage(page, limit, count, request);
|
||||
|
||||
return {
|
||||
pager,
|
||||
...response,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
//******* Extract Pagination Params *******//
|
||||
private extractPaginationParams(query: PaginationQuery): { page: number; limit: number } {
|
||||
const page = this.validateNumber(query.page, this.DEFAULT_PAGE);
|
||||
const limit = this.validateNumber(query.limit, this.DEFAULT_LIMIT);
|
||||
|
||||
return {
|
||||
page: Math.max(1, page),
|
||||
limit: Math.min(Math.max(1, limit), this.MAX_LIMIT),
|
||||
};
|
||||
}
|
||||
|
||||
//******* Validate Number *******//
|
||||
private validateNumber(value: string | undefined, defaultValue: number): number {
|
||||
const parsed = parseInt(value || "", 10);
|
||||
return isNaN(parsed) ? defaultValue : parsed;
|
||||
}
|
||||
|
||||
//******* Get Context Info *******//
|
||||
private getContextInfo(context: ExecutionContext): { className: string; handlerName: string } {
|
||||
return {
|
||||
className: context.getClass().name,
|
||||
handlerName: context.getHandler().name,
|
||||
};
|
||||
}
|
||||
|
||||
//******* Check if Response is Paginated *******//
|
||||
private isPaginatedResponse(data: PaginatedResponse): data is PaginatedResponse {
|
||||
return data && (data.paginate || typeof data.count === "number");
|
||||
}
|
||||
|
||||
//******* Format Page *******//
|
||||
private formatPage(page: number, limit: number, totalItems: number | undefined, request: FastifyRequest): IPageFormat {
|
||||
const count = totalItems || 0;
|
||||
const totalPages = Math.ceil(count / limit);
|
||||
const prevPage = page > 1 ? page - 1 : false;
|
||||
const nextPage = page < totalPages ? page + 1 : false;
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
totalItems: count,
|
||||
totalPages,
|
||||
prevPage: this.generatePageLink(prevPage, limit, request),
|
||||
nextPage: this.generatePageLink(nextPage, limit, request),
|
||||
};
|
||||
}
|
||||
|
||||
//******* Generate Page Link *******//
|
||||
private generatePageLink(page: number | boolean, limit: number, request: FastifyRequest): string | boolean {
|
||||
if (!page) return false;
|
||||
|
||||
const { protocol, hostname, url } = request;
|
||||
const baseUrl = url.split("?")[0];
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: limit.toString(),
|
||||
});
|
||||
|
||||
return `${protocol}://${hostname}${baseUrl}?${queryParams.toString()}`;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
|
||||
import { FastifyReply } from "fastify";
|
||||
import { Observable } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor implements NestInterceptor {
|
||||
//
|
||||
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
|
||||
const ctx = context.switchToHttp().getResponse<FastifyReply>();
|
||||
const statusCode = ctx.statusCode;
|
||||
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
if (data && data.data !== undefined) {
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data: data.data,
|
||||
};
|
||||
}
|
||||
return {
|
||||
statusCode,
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
import { Injectable, Logger, NestMiddleware } from "@nestjs/common";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
@Injectable()
|
||||
export class HTTPLogger implements NestMiddleware {
|
||||
private readonly logger = new Logger(HTTPLogger.name);
|
||||
|
||||
use(req: FastifyRequest, rep: FastifyReply["raw"], next: () => void): void {
|
||||
const startAt = process.hrtime();
|
||||
const { method, originalUrl } = req;
|
||||
const ip = req.ip || req.socket.remoteAddress || "-";
|
||||
const userAgent = req.headers["user-agent"] || "-";
|
||||
const referer = req.headers.referer || "-";
|
||||
|
||||
rep.on("finish", () => {
|
||||
const { statusCode } = rep;
|
||||
const contentLength = rep.getHeader("content-length") || 0;
|
||||
const dif = process.hrtime(startAt);
|
||||
const responseTime = dif[0] * 1e3 + dif[1] * 1e-6;
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Apache-like format: IP - - [timestamp] "METHOD URL" STATUS SIZE "REFERER" "USER-AGENT" RESPONSE_TIME
|
||||
this.logger.log(
|
||||
`${ip} - - [${timestamp}] "${method} ${originalUrl}" ${statusCode} ${contentLength} "${referer}" "${userAgent}" ${responseTime.toFixed(2)}ms`,
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { MikroORM } from "@mikro-orm/core";
|
||||
import { Logger, ValidationPipe } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
import { getSwaggerDocument } from "./configs/swagger.config";
|
||||
import { HttpExceptionFilter } from "./core/filters/http-exception.filters";
|
||||
import { PaginationInterceptor } from "./core/interceptors/pagination.interceptor";
|
||||
import { ResponseInterceptor } from "./core/interceptors/response.interceptor";
|
||||
async function bootstrap() {
|
||||
const logger = new Logger("APP");
|
||||
|
||||
const fastifyAdapter = new FastifyAdapter({
|
||||
bodyLimit: 10 * 1024 * 1024,
|
||||
trustProxy: true,
|
||||
});
|
||||
|
||||
fastifyAdapter.getInstance().addContentTypeParser("multipart/form-data", (_request, _payload, done) => {
|
||||
done(null);
|
||||
});
|
||||
|
||||
fastifyAdapter.getInstance().addContentTypeParser("text/plain", (_request, _payload, done) => {
|
||||
done(null);
|
||||
});
|
||||
|
||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, fastifyAdapter);
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
|
||||
app.useGlobalInterceptors(new ResponseInterceptor(), new PaginationInterceptor());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
app.enableCors({
|
||||
origin: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 204,
|
||||
});
|
||||
const configService = app.get<ConfigService>(ConfigService);
|
||||
|
||||
getSwaggerDocument(app);
|
||||
|
||||
const orm = app.get(MikroORM);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
await app.get(MikroORM).getSchemaGenerator().ensureDatabase();
|
||||
await orm.getSchemaGenerator().updateSchema();
|
||||
}
|
||||
|
||||
const PORT = configService.getOrThrow<number>("PORT");
|
||||
await app.listen(PORT, "0.0.0.0", () => {
|
||||
logger.log(`Server running at http://localhost:${PORT}`);
|
||||
logger.log(`Swagger is serving at http://localhost:${PORT}/api-docs`);
|
||||
});
|
||||
}
|
||||
bootstrap();
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString, MinLength } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class ChangePasswordDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "old password", example: "12S345SS678" })
|
||||
@MinLength(8, { message: AuthMessage.PASSWORD_LENGTH })
|
||||
password: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "new password", example: "12S345SS678" })
|
||||
@MinLength(8, { message: AuthMessage.PASSWORD_LENGTH })
|
||||
newPassword: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "re new password", example: "12S345SS678" })
|
||||
@MinLength(8, { message: AuthMessage.PASSWORD_LENGTH })
|
||||
repeatPassword: string;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsNumberString, IsString, Length, MinLength } from "class-validator";
|
||||
import { IsMobilePhone } from "class-validator";
|
||||
|
||||
import { IsNationalCode } from "../../../common/decorators/is-national-code.decorator";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CompleteRegistrationDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY })
|
||||
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
|
||||
@Length(11, 11, { message: AuthMessage.PHONE_SHOULD_BE_11_DIGIT })
|
||||
@ApiProperty({ description: "phone number", default: "09922320740" })
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({ description: "OTP code received via SMS", example: "56893" })
|
||||
@IsNotEmpty({ message: AuthMessage.OTP_NOT_EMPTY })
|
||||
@IsNumberString(undefined, { message: AuthMessage.OTP_FORMAT_INVALID })
|
||||
@Length(5, 5, { message: AuthMessage.OTP_FORMAT_INVALID })
|
||||
code: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
|
||||
@Length(2, 50, { message: AuthMessage.FIRST_NAME_SHOULD_BE_BETWEEN_2_AND_50 })
|
||||
@ApiProperty({ description: "First name", example: "mahyar" })
|
||||
firstName: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.LAST_NAME_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.LAST_NAME_NOT_EMPTY })
|
||||
@Length(2, 50, { message: AuthMessage.LAST_NAME_SHOULD_BE_BETWEEN_2_AND_50 })
|
||||
@ApiProperty({ description: "Last name", example: "jamshidi" })
|
||||
lastName: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.BIRTH_DATE_NOT_EMPTY })
|
||||
@ApiProperty({ description: "Birth date", example: "1403/01/01" })
|
||||
birthDate: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.NATIONAL_NOT_EMPTY })
|
||||
@IsNumberString({ no_symbols: true }, { message: AuthMessage.NATIONAL_CODE_INCORRECT })
|
||||
@Length(10, 10, { message: AuthMessage.NATIONAL_CODE_INCORRECT })
|
||||
@IsNationalCode({ message: AuthMessage.NATIONAL_CODE_INVALID })
|
||||
@ApiProperty({ description: "iranian format (10 char)", example: "4569852169" })
|
||||
nationalCode: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.PASSWORD_FORMAT_INVALID })
|
||||
@ApiProperty({ description: "password", example: "12S345SS678" })
|
||||
@MinLength(8, { message: AuthMessage.PASSWORD_LENGTH })
|
||||
password: string;
|
||||
}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty, PickType } from "@nestjs/swagger";
|
||||
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class LoginPasswordDTO {
|
||||
@IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
|
||||
@IsEmail(undefined, { message: AuthMessage.INVALID_EMAIL_FORMAT })
|
||||
@ApiProperty({ description: "email of user", default: "admin-dsc@gmail.com" })
|
||||
email: string;
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.PasswordNotEmpty })
|
||||
@IsString({ message: AuthMessage.INVALID_PASS_FORMAT })
|
||||
@ApiProperty({ type: "string", description: "password user", example: "admin123" })
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class CheckUserExistDto extends PickType(LoginPasswordDTO, ["email"] as const) {}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty({ description: "Refresh token" })
|
||||
@IsString({ message: "Refresh token must be a string" })
|
||||
@IsNotEmpty({ message: "Refresh token is required" })
|
||||
refreshToken: string;
|
||||
}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsMobilePhone, IsNotEmpty, Length } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class RequestOtpDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY })
|
||||
@Length(11, 11, { message: AuthMessage.PHONE_SHOULD_BE_11_DIGIT })
|
||||
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
|
||||
@ApiProperty({ description: "phone number", default: "09922320740" })
|
||||
phone: string;
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import { ApiProperty, PickType } from "@nestjs/swagger";
|
||||
import { IsMobilePhone, IsNotEmpty, IsNumberString, Length } from "class-validator";
|
||||
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class VerifyOtpDto {
|
||||
@IsNotEmpty({ message: AuthMessage.PHONE_NOT_EMPTY })
|
||||
@Length(11, 11, { message: AuthMessage.PHONE_SHOULD_BE_11_DIGIT })
|
||||
@IsMobilePhone("fa-IR", {}, { message: AuthMessage.INVALID_PHONE_FORMAT })
|
||||
@ApiProperty({ description: "phone number", default: "09922320740" })
|
||||
phone: string;
|
||||
|
||||
@ApiProperty({ description: "OTP code received via SMS", example: "56893" })
|
||||
@IsNotEmpty({ message: AuthMessage.OTP_NOT_EMPTY })
|
||||
@IsNumberString(undefined, { message: AuthMessage.OTP_FORMAT_INVALID })
|
||||
@Length(5, 5, { message: AuthMessage.OTP_FORMAT_INVALID })
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class VerifyOtpWithUserId extends PickType(VerifyOtpDto, ["code"]) {}
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, Patch, Post, UseGuards } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { Throttle, ThrottlerGuard } from "@nestjs/throttler";
|
||||
|
||||
import { ChangePasswordDto } from "./DTO/change-password.dto";
|
||||
import { CompleteRegistrationDto } from "./DTO/complete-register.dto";
|
||||
import { CheckUserExistDto, LoginPasswordDTO } from "./DTO/loginPassword.dto";
|
||||
import { RefreshTokenDto } from "./DTO/refresh-token.dto";
|
||||
import { RequestOtpDto } from "./DTO/request-otp.dto";
|
||||
import { VerifyOtpDto } from "./DTO/verify-otp.dto";
|
||||
import { AuthService } from "./providers/auth.service";
|
||||
import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL, AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL } from "../../common/constants";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
|
||||
@ApiTags("Auth")
|
||||
@Controller("auth")
|
||||
@Throttle({ default: { limit: AUTH_THROTTLE_LIMIT, ttl: AUTH_THROTTLE_TTL } })
|
||||
@UseGuards(ThrottlerGuard)
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@ApiOperation({ summary: "Initiate registration" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("register/initiate")
|
||||
register(@Body() registerDto: RequestOtpDto) {
|
||||
return this.authService.initiateRegistration(registerDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "complete registration ==> step 2" })
|
||||
@Post("register/complete")
|
||||
completeRegistration(@Body() completeRegistrationDto: CompleteRegistrationDto) {
|
||||
return this.authService.completeRegistration(completeRegistrationDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "request to login with otp" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("otp/send")
|
||||
sendOtp(@Body() requestOtpDto: RequestOtpDto) {
|
||||
return this.authService.requestLoginOtp(requestOtpDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "verify otp for login" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("otp/verify")
|
||||
verifyOtp(@Body() verifyOtpDto: VerifyOtpDto) {
|
||||
return this.authService.verifyLoginOtp(verifyOtpDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "check if user email exist" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("check")
|
||||
checkUserExist(@Body() checkUserExistDto: CheckUserExistDto) {
|
||||
return this.authService.checkUserExist(checkUserExistDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "login with password" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("login/password")
|
||||
loginWithPassword(@Body() loginPasswordDto: LoginPasswordDTO) {
|
||||
return this.authService.loginWithPassword(loginPasswordDto);
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "Update user password" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Patch("change-password")
|
||||
changePassword(@UserDec("id") userId: string, @Body() changePasswordDto: ChangePasswordDto) {
|
||||
return this.authService.changePassword(userId, changePasswordDto);
|
||||
}
|
||||
|
||||
@Throttle({ default: { limit: AUTH__REFRESH_THROTTLE_LIMIT, ttl: AUTH__REFRESH_THROTTLE_TTL } })
|
||||
@ApiOperation({ summary: "refresh the user access token / refresh token" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("refresh")
|
||||
refresh(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
||||
}
|
||||
|
||||
@AuthGuards()
|
||||
@ApiOperation({ summary: "logout the user" })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("logout")
|
||||
logout(@UserDec("id") userId: string) {
|
||||
return this.authService.logout(userId);
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
// @Post("forgot-password")
|
||||
// async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
|
||||
// return this.authService.forgotPassword(forgotPasswordDto);
|
||||
// }
|
||||
|
||||
// @Post("reset-password")
|
||||
// async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
|
||||
// return this.authService.resetPassword(resetPasswordDto);
|
||||
// }
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { jwtConfig } from "../../configs/jwt.config";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { AuthService } from "./providers/auth.service";
|
||||
import { TokensService } from "./providers/tokens.service";
|
||||
import { JwtStrategy } from "./strategies/jwt.strategy";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), NotificationModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, TokensService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { ADMIN_ROUTE } from "../../../common/decorators/admin.decorator";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@Injectable()
|
||||
export class AdminRouteGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredAdmin = this.reflector.getAllAndOverride<boolean>(ADMIN_ROUTE, [context.getHandler(), context.getClass()]);
|
||||
if (!requiredAdmin) return true;
|
||||
|
||||
const req = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
const user = req.user;
|
||||
|
||||
if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
if (!user.role) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
import { ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { SKIP_AUTH_KEY } from "../../../common/decorators/skip-auth.decorator";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const skipAuth = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH_KEY, [context.getHandler(), context.getClass()]);
|
||||
|
||||
if (skipAuth) return true;
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: unknown, user: any, _info: unknown) {
|
||||
if (err || !user) throw new UnauthorizedException(AuthMessage.TOKEN_EXPIRED_OR_INVALID);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
// import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
// import { Reflector } from "@nestjs/core";
|
||||
// import { FastifyRequest } from "fastify";
|
||||
|
||||
// import { PERMISSION_KEY } from "../../../common/decorators/permission.decorator";
|
||||
// import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
// import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
|
||||
// @Injectable()
|
||||
// export class PermissionsGuard implements CanActivate {
|
||||
// constructor(private reflector: Reflector) {}
|
||||
|
||||
// canActivate(context: ExecutionContext) {
|
||||
// const requiredPermissions = this.reflector.getAllAndOverride<PermissionEnum[]>(PERMISSION_KEY, [
|
||||
// context.getHandler(),
|
||||
// context.getClass(),
|
||||
// ]);
|
||||
|
||||
// if (!requiredPermissions) return true;
|
||||
|
||||
// const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const user = request.user;
|
||||
|
||||
// if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// const hasPermission = requiredPermissions.every((perm) => user.permissions.includes(perm));
|
||||
|
||||
// if (!hasPermission) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
// import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||
// import { Reflector } from "@nestjs/core";
|
||||
// import { FastifyRequest } from "fastify";
|
||||
|
||||
// import { ROLES_KEY } from "../../../common/decorators/roles.decorator";
|
||||
// import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
// import { RoleEnum } from "../../users/enums/role.enum";
|
||||
|
||||
// @Injectable()
|
||||
// export class RoleGuard implements CanActivate {
|
||||
// constructor(private reflector: Reflector) {}
|
||||
|
||||
// canActivate(context: ExecutionContext): boolean {
|
||||
// const requiredRole = this.reflector.getAllAndOverride<RoleEnum[]>(ROLES_KEY, [context.getHandler(), context.getClass()]);
|
||||
// if (!requiredRole) return true;
|
||||
|
||||
// const req = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
// const user = req.user;
|
||||
|
||||
// if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// const hasRequiredRole = requiredRole.some((role) => user.roles.includes(role));
|
||||
// if (!hasRequiredRole) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
|
||||
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
|
||||
export interface ITokenPayload {
|
||||
id: string;
|
||||
role: RoleEnum;
|
||||
}
|
||||
Executable
+227
@@ -0,0 +1,227 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
|
||||
import { TokensService } from "./tokens.service";
|
||||
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { Role } from "../../users/entities/role.entity";
|
||||
import { RoleEnum } from "../../users/enums/role.enum";
|
||||
import { UsersService } from "../../users/services/users.service";
|
||||
import { OTPService } from "../../utils/providers/otp.service";
|
||||
import { PasswordService } from "../../utils/providers/password.service";
|
||||
import { SmsService } from "../../utils/providers/sms.service";
|
||||
import { ChangePasswordDto } from "../DTO/change-password.dto";
|
||||
import { CompleteRegistrationDto } from "../DTO/complete-register.dto";
|
||||
import { CheckUserExistDto, LoginPasswordDTO } from "../DTO/loginPassword.dto";
|
||||
import { RequestOtpDto } from "../DTO/request-otp.dto";
|
||||
import { VerifyOtpDto } from "../DTO/verify-otp.dto";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly otpService: OTPService,
|
||||
private readonly tokensService: TokensService,
|
||||
private readonly notificationQueue: NotificationQueue,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
//****************** */
|
||||
//****************** */
|
||||
async initiateRegistration(requestOtpDto: RequestOtpDto) {
|
||||
const { phone } = requestOtpDto;
|
||||
const existUser = await this.usersService.findOneWithPhone(phone);
|
||||
if (existUser) throw new BadRequestException(AuthMessage.PHONE_EXISTS);
|
||||
|
||||
const existCode = await this.otpService.checkExistOtp(phone, "REGISTER");
|
||||
if (existCode) {
|
||||
return {
|
||||
message: AuthMessage.OTP_ALREADY_SENT,
|
||||
ttlSecond: existCode,
|
||||
};
|
||||
}
|
||||
const otpCode = await this.otpService.generateAndSetInCache(phone, "REGISTER");
|
||||
//
|
||||
await this.notificationQueue.addOtpNotification(phone, { userPhone: phone, otp: otpCode });
|
||||
await this.smsService.sendSmsVerifyCode(phone, otpCode);
|
||||
|
||||
return {
|
||||
message: AuthMessage.OTP_SENT,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
//****************** */
|
||||
async completeRegistration(completeRegistrationDto: CompleteRegistrationDto) {
|
||||
const { phone, code } = completeRegistrationDto;
|
||||
|
||||
const entityManager = this.em.fork();
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
//
|
||||
const isValid = await this.otpService.verifyOtp(phone, code, "REGISTER");
|
||||
|
||||
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
|
||||
|
||||
await this.otpService.delOtpFormCache(phone, "REGISTER");
|
||||
const hashedPassword = await this.passwordService.hashPassword(completeRegistrationDto.password);
|
||||
|
||||
const user = await this.usersService.createUser(completeRegistrationDto, hashedPassword, entityManager);
|
||||
|
||||
const tokens = await this.tokensService.generateTokens(user, entityManager);
|
||||
|
||||
await entityManager.commit();
|
||||
|
||||
return {
|
||||
message: AuthMessage.USER_REGISTER_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
} catch (error) {
|
||||
await entityManager.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
//****************** */
|
||||
//****************** */
|
||||
async loginWithPassword(loginDto: LoginPasswordDTO) {
|
||||
const { email, password } = loginDto;
|
||||
|
||||
const user = await this.checkUserLoginCredentialWithEmail(email, password);
|
||||
|
||||
if (this.checkUserIsAdmin(user.role)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN);
|
||||
|
||||
const tokens = await this.tokensService.generateTokens(user);
|
||||
|
||||
await this.notificationQueue.addLoginNotification(user.id, { userPhone: user.phone });
|
||||
|
||||
return {
|
||||
message: AuthMessage.PASSWORD_LOGIN_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
async checkUserExist(checkUserExistDto: CheckUserExistDto) {
|
||||
const user = await this.usersService.findOneWithEmail(checkUserExistDto.email);
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { message: UserMessage.USER_EXISTS };
|
||||
}
|
||||
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
async requestLoginOtp(requestOtpDto: RequestOtpDto, isAdmin: boolean = false) {
|
||||
const { phone } = requestOtpDto;
|
||||
const user = await this.usersService.findOneWithPhone(phone);
|
||||
if (!user) throw new BadRequestException(AuthMessage.PHONE_NOT_FOUND);
|
||||
|
||||
//check the if the method call is from admin or not
|
||||
|
||||
const isUserAdmin = this.checkUserIsAdmin(user.role);
|
||||
|
||||
if (isAdmin && !isUserAdmin) throw new BadRequestException(AuthMessage.NOT_ADMIN);
|
||||
|
||||
const existCode = await this.otpService.checkExistOtp(phone, "LOGIN");
|
||||
if (existCode) {
|
||||
return {
|
||||
message: AuthMessage.OTP_ALREADY_SENT,
|
||||
ttlSecond: existCode,
|
||||
};
|
||||
}
|
||||
|
||||
const otpCode = await this.otpService.generateAndSetInCache(phone, "LOGIN");
|
||||
//
|
||||
await this.smsService.sendSmsVerifyCode(phone, otpCode);
|
||||
|
||||
return {
|
||||
message: AuthMessage.OTP_SENT,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
async verifyLoginOtp(verifyOtpDto: VerifyOtpDto) {
|
||||
const { code, phone } = verifyOtpDto;
|
||||
|
||||
const user = await this.checkUserLoginCredentialWithPhone(phone, code);
|
||||
|
||||
if (this.checkUserIsAdmin(user.role)) throw new BadRequestException(AuthMessage.ADMIN_CAN_NOT_LOGIN);
|
||||
|
||||
const tokens = await this.tokensService.generateTokens(user);
|
||||
|
||||
await this.notificationQueue.addLoginNotification(user.id, { userPhone: user.phone, userEmail: user.email });
|
||||
return {
|
||||
message: AuthMessage.LOGIN_SUCCESS,
|
||||
...tokens,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
|
||||
async changePassword(userId: string, updateDto: ChangePasswordDto) {
|
||||
const { user } = await this.usersService.findOneById(userId);
|
||||
|
||||
const passCompare = await this.passwordService.comparePassword(updateDto.password, user.password);
|
||||
if (!passCompare) throw new BadRequestException(AuthMessage.CURRENT_PASSWORD_INVALID);
|
||||
|
||||
if (updateDto.newPassword !== updateDto.repeatPassword) throw new BadRequestException(AuthMessage.INVALID_REPEAT_PASSWORD);
|
||||
|
||||
const hashedPassword = await this.passwordService.hashPassword(updateDto.newPassword);
|
||||
await this.usersService.updateUserPassword(user, hashedPassword);
|
||||
|
||||
return {
|
||||
message: AuthMessage.PASSWORD_CHANGED_SUCCESSFULLY,
|
||||
};
|
||||
}
|
||||
//****************** */
|
||||
|
||||
async refreshToken(oldRefreshToken: string) {
|
||||
return this.tokensService.refreshToken(oldRefreshToken);
|
||||
}
|
||||
//****************** */
|
||||
|
||||
async logout(userId: string) {
|
||||
await this.tokensService.invalidateRefreshToken(userId);
|
||||
return {
|
||||
message: AuthMessage.LOGOUT_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//****************** */
|
||||
|
||||
private async checkUserLoginCredentialWithEmail(email: string, password: string) {
|
||||
const user = await this.usersService.findOneWithEmail(email);
|
||||
if (!user) throw new BadRequestException(AuthMessage.INVALID_PASSWORD);
|
||||
|
||||
const passCompare = await this.passwordService.comparePassword(password, user.password);
|
||||
if (!passCompare) throw new BadRequestException(AuthMessage.INVALID_PASSWORD);
|
||||
|
||||
return user;
|
||||
}
|
||||
//****************** */
|
||||
//****************** */
|
||||
|
||||
private async checkUserLoginCredentialWithPhone(phone: string, otpCode: string) {
|
||||
const isValid = await this.otpService.verifyOtp(phone, otpCode, "LOGIN");
|
||||
if (!isValid) throw new BadRequestException(AuthMessage.INVALID_OTP);
|
||||
|
||||
await this.otpService.delOtpFormCache(phone, "LOGIN");
|
||||
|
||||
const user = await this.usersService.findOneWithPhone(phone);
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
return user;
|
||||
}
|
||||
// private checkUserPerm(user: User, perm: PermissionEnum) {
|
||||
// return user.roles.some((role) => role?.permissions?.some((p) => p.name === perm));
|
||||
// }
|
||||
//****************** */
|
||||
//****************** */
|
||||
private checkUserIsAdmin(role: Role) {
|
||||
return role.name === RoleEnum.ADMIN;
|
||||
}
|
||||
}
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { RefreshToken } from "../../users/entities/refresh-token.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
|
||||
@Injectable()
|
||||
export class TokensService {
|
||||
private readonly logger = new Logger(TokensService.name);
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
async generateTokens(user: User, em?: EntityManager) {
|
||||
return this.generateAccessAndRefreshToken(
|
||||
{
|
||||
id: user.id,
|
||||
role: user.role.name,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
private async generateAccessAndRefreshToken(payload: ITokenPayload, em?: EntityManager) {
|
||||
const accessExpire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE");
|
||||
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
|
||||
|
||||
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: `${accessExpire}m` });
|
||||
const refreshToken = await this.generateRefreshToken();
|
||||
|
||||
await this.storeRefreshToken(payload.id, refreshToken, em);
|
||||
|
||||
return {
|
||||
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, "minute").valueOf() },
|
||||
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, "day").valueOf() },
|
||||
};
|
||||
}
|
||||
|
||||
async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) {
|
||||
const entityManager = em || this.em.fork();
|
||||
let transactionStarted = false;
|
||||
|
||||
try {
|
||||
if (!entityManager.isInTransaction()) {
|
||||
await entityManager.begin();
|
||||
transactionStarted = true;
|
||||
}
|
||||
|
||||
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
|
||||
const expiresAt = dayjs().add(refreshExpire, "day").toDate();
|
||||
|
||||
const user = await entityManager.findOne(User, { id: userId });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
const token = entityManager.create(RefreshToken, {
|
||||
token: refreshToken,
|
||||
user,
|
||||
expiresAt,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await entityManager.persistAndFlush(token);
|
||||
|
||||
if (transactionStarted) await entityManager.commit();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (transactionStarted) await entityManager.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshToken(oldRefreshToken: string) {
|
||||
const entityManager = this.em.fork();
|
||||
let transactionStarted = false;
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
transactionStarted = true;
|
||||
|
||||
const token = await entityManager.findOne(
|
||||
RefreshToken,
|
||||
{ token: oldRefreshToken },
|
||||
{
|
||||
populate: ["user", "user.role"],
|
||||
},
|
||||
);
|
||||
|
||||
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
|
||||
|
||||
if (dayjs(token.expiresAt).isBefore(dayjs())) {
|
||||
await entityManager.removeAndFlush(token);
|
||||
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
|
||||
}
|
||||
|
||||
await entityManager.removeAndFlush(token);
|
||||
const tokens = await this.generateTokens(token.user, entityManager);
|
||||
|
||||
await entityManager.commit();
|
||||
return tokens;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (transactionStarted) {
|
||||
await entityManager.rollback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateRefreshToken(userId: string) {
|
||||
const entityManager = this.em.fork();
|
||||
let transactionStarted = false;
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
transactionStarted = true;
|
||||
|
||||
await entityManager.nativeDelete(RefreshToken, { user: { id: userId } });
|
||||
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
|
||||
|
||||
await entityManager.commit();
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (transactionStarted) {
|
||||
await entityManager.rollback();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async generateRefreshToken() {
|
||||
return randomBytes(32).toString("hex");
|
||||
}
|
||||
}
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
|
||||
import { JWT_STRATEGY_NAME } from "../../../common/constants";
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.getOrThrow<string>("JWT_SECRET"),
|
||||
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: ITokenPayload) {
|
||||
return { id: payload.id, role: payload.role };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IsEnum, IsNotEmpty, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { NotificationMessage } from "../../../common/enums/message.enum";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
|
||||
export class CreateNotificationDto {
|
||||
@IsNotEmpty({ message: NotificationMessage.TITLE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.TITLE_STRING })
|
||||
title: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.MESSAGE_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.MESSAGE_STRING })
|
||||
message: string;
|
||||
|
||||
@IsNotEmpty({ message: NotificationMessage.USERID_IS_REQUIRED })
|
||||
@IsString({ message: NotificationMessage.USERID_IS_STRING })
|
||||
@IsUUID("7", { message: NotificationMessage.USERID_IS_UUID })
|
||||
recipientId: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEnum(NotifType)
|
||||
type: NotifType;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsIn, IsOptional } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export class SearchNotificationQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsIn([0, 1])
|
||||
@ApiPropertyOptional({ description: "Notification status", example: 1 })
|
||||
isRead?: number;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const NOTIFICATION = Object.freeze({
|
||||
QUEUE_NAME: "notification",
|
||||
SEND_NOTIFICATION_JOB_NAME: "sendNotification",
|
||||
|
||||
SEND_NOTIFICATION_JOB_DELAY: 1000, // delay after 1 second
|
||||
SEND_NOTIFICATION_JOB_PRIORITY: 1, // high priority
|
||||
SEND_NOTIFICATION_JOB_ATTEMPTS: 3, // retry 3 times
|
||||
SEND_NOTIFICATION_JOB_BACKOFF: 5 * 1000, // retry after 5 seconds
|
||||
SEND_NOTIFICATION_JOB_TIMEOUT: 10000, // timeout after 10 seconds
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Entity({ repository: () => NotificationRepository })
|
||||
export class Notification extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 250, nullable: false })
|
||||
title!: string;
|
||||
|
||||
@Property({ type: "text", nullable: false })
|
||||
message!: string;
|
||||
|
||||
@Enum({ items: () => NotifType, nativeEnumName: "notif_type", nullable: false })
|
||||
type: NotifType;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
isRead: boolean & Opt;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "cascade" })
|
||||
recipient!: User;
|
||||
|
||||
[EntityRepositoryType]?: NotificationRepository;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
export enum NotifType {
|
||||
// Account category
|
||||
OTP = "OTP",
|
||||
USER_LOGIN = "USER_LOGIN",
|
||||
ANNOUNCEMENT = "ANNOUNCEMENT",
|
||||
|
||||
// Finance category
|
||||
WALLET_CHARGE = "WALLET_CHARGE",
|
||||
WALLET_DEDUCTION = "WALLET_DEDUCTION",
|
||||
|
||||
//Invoice
|
||||
BILL_INVOICE_REMINDER = "BILL_INVOICE_REMINDER",
|
||||
BILL_INVOICE = "BILL_INVOICE",
|
||||
APPROVE_INVOICE = "APPROVE_INVOICE",
|
||||
CREATE_INVOICE = "CREATE_INVOICE",
|
||||
INVOICE_OVERDUE = "INVOICE_OVERDUE",
|
||||
|
||||
// Service category
|
||||
CREATE_SERVICE = "CREATE_SERVICE",
|
||||
UNBLOCK_SERVICE = "UNBLOCK_SERVICE",
|
||||
BLOCK_SERVICE = "BLOCK_SERVICE",
|
||||
|
||||
// Support category
|
||||
ANSWER_TICKET = "ANSWER_TICKET",
|
||||
CREATE_TICKET = "CREATE_TICKET",
|
||||
//
|
||||
ASSIGN_TICKET = "ASSIGN_TICKET",
|
||||
RECURRING_INVOICE = "RECURRING_INVOICE",
|
||||
PAYMENT_REMINDER = "PAYMENT_REMINDER",
|
||||
PAYMENT_CANCELLATION = "PAYMENT_CANCELLATION",
|
||||
|
||||
// // Admin category
|
||||
|
||||
NEW_CUSTOMER = "NEW_CUSTOMER",
|
||||
NEW_TICKET = "NEW_TICKET",
|
||||
NEW_CRITICISM = "NEW_CRITICISM",
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export interface IBaseNotificationData {
|
||||
userPhone: string;
|
||||
userEmail?: string;
|
||||
}
|
||||
|
||||
export interface IOtpNotificationData extends IBaseNotificationData {
|
||||
otp: string;
|
||||
}
|
||||
|
||||
export interface IInvoiceNotificationData extends IBaseNotificationData {
|
||||
price: number;
|
||||
items: string;
|
||||
dueDate: Date;
|
||||
createDate: Date;
|
||||
invoiceId: string;
|
||||
paidAt?: Date;
|
||||
lateFee?: number;
|
||||
}
|
||||
|
||||
export interface ITicketNotificationData extends IBaseNotificationData {
|
||||
ticketId: string;
|
||||
subject: string;
|
||||
date: Date;
|
||||
user?: string;
|
||||
}
|
||||
|
||||
export interface IGenericNotificationData extends IBaseNotificationData {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IAnnouncementNotificationData extends IBaseNotificationData {
|
||||
title: string;
|
||||
description: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface IPaymentNotificationData extends IBaseNotificationData {
|
||||
amount: number;
|
||||
paymentId: string;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Controller, Get, Param, Patch, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { SearchNotificationQueryDto } from "./DTO/search-notification-query.dto";
|
||||
import { NotificationsService } from "./providers/notifications.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { Pagination } from "../../common/decorators/pagination.decorator";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
|
||||
@Controller("notifications")
|
||||
@ApiTags("Notifications")
|
||||
@AuthGuards()
|
||||
export class NotificationController {
|
||||
constructor(private readonly notificationService: NotificationsService) {}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "all notifications by user" })
|
||||
@Pagination()
|
||||
@Get()
|
||||
getAllNotifications(@UserDec("id") userId: string, @Query() queryDto: SearchNotificationQueryDto) {
|
||||
return this.notificationService.getAllNotifications(queryDto, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "mark user notification as read" })
|
||||
@Patch(":id/read")
|
||||
markAsRead(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
return this.notificationService.markAsRead(paramDto.id, userId);
|
||||
}
|
||||
|
||||
//********************* */
|
||||
|
||||
@ApiOperation({ summary: "mark user all notification as read" })
|
||||
@Patch("read-all")
|
||||
markAllAsRead(@UserDec("id") userId: string) {
|
||||
return this.notificationService.markAllAsRead(userId);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
import { NOTIFICATION } from "./constants";
|
||||
import { UtilsModule } from "../utils/utils.module";
|
||||
import { Notification } from "./entities/notification.entity";
|
||||
import { NotificationController } from "./notifications.controller";
|
||||
import { NotificationsService } from "./providers/notifications.service";
|
||||
import { NotificationProcessor } from "./queue/notification.processor";
|
||||
import { NotificationQueue } from "./queue/notification.queue";
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
MikroOrmModule.forFeature([Notification]),
|
||||
BullModule.registerQueue({
|
||||
name: NOTIFICATION.QUEUE_NAME,
|
||||
defaultJobOptions: {
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
},
|
||||
}),
|
||||
UtilsModule,
|
||||
],
|
||||
providers: [NotificationsService, NotificationQueue, NotificationProcessor],
|
||||
controllers: [NotificationController],
|
||||
exports: [NotificationsService, NotificationQueue],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { CommonMessage, NotificationMessage } from "../../../common/enums/message.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { EmailService } from "../../utils/providers/email.service";
|
||||
import { dateFormat, numberFormat } from "../../utils/providers/international.utils";
|
||||
import { SmsService } from "../../utils/providers/sms.service";
|
||||
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IBaseNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
private readonly logger = new Logger(NotificationsService.name);
|
||||
constructor(
|
||||
private readonly notificationRepository: NotificationRepository,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createNotification(createNotificationDto: CreateNotificationDto, em?: EntityManager) {
|
||||
if (!em) {
|
||||
const localEm = this.em.fork();
|
||||
|
||||
try {
|
||||
await localEm.begin();
|
||||
//
|
||||
const notification = localEm.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: localEm.getReference(User, createNotificationDto.recipientId),
|
||||
});
|
||||
await localEm.persistAndFlush(notification);
|
||||
|
||||
await localEm.commit();
|
||||
|
||||
return notification;
|
||||
//
|
||||
} catch (error) {
|
||||
this.logger.error("error in createNotification", error);
|
||||
await localEm.rollback();
|
||||
throw error;
|
||||
//
|
||||
}
|
||||
//
|
||||
} else {
|
||||
//
|
||||
const notification = em.create(Notification, {
|
||||
...createNotificationDto,
|
||||
recipient: em.getReference(User, createNotificationDto.recipientId),
|
||||
});
|
||||
//
|
||||
await em.persistAndFlush(notification);
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAsRead(notificationId: string, userId: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId } });
|
||||
|
||||
if (!notification) throw new NotFoundException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
|
||||
notification.isRead = true;
|
||||
await this.em.flush();
|
||||
return notification;
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
return await this.notificationRepository.getAllNotifications(queryDto, recipientId);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async getNotificationById(id: string) {
|
||||
const notification = await this.notificationRepository.findOne({ id });
|
||||
if (!notification) throw new BadRequestException(NotificationMessage.NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED);
|
||||
return { notification };
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async countUserNotifications(userId: string) {
|
||||
const notificationCount = await this.notificationRepository.count({ recipient: { id: userId }, isRead: false });
|
||||
return notificationCount;
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async markAllAsRead(userId: string) {
|
||||
await this.em.nativeUpdate(Notification, { recipient: { id: userId } }, { isRead: true });
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
};
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createOtpNotification(data: IOtpNotificationData) {
|
||||
//sent notification based on the userSettings
|
||||
return await this.smsService.sendSmsVerifyCode(data.userPhone, data.otp);
|
||||
}
|
||||
|
||||
async createLoginNotification(recipientId: string, data: IBaseNotificationData) {
|
||||
const loginDate = new Date().toLocaleString("fa-IR");
|
||||
const message = NotificationMessage.LOGIN_MESSAGE.replace("[loginDate]", loginDate);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendLoginSms(data.userPhone, loginDate);
|
||||
// if (data.userEmail) await this.emailService.sendLoginEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.LOGIN, type: NotifType.USER_LOGIN, message, recipientId });
|
||||
}
|
||||
|
||||
//************************ */
|
||||
async createInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.INVOICE_CREATION_MESSAGE.replace("[id]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoiceCreationSms(
|
||||
data.userPhone,
|
||||
data.invoiceId,
|
||||
data.price,
|
||||
data.items,
|
||||
dateFormat(data.createDate),
|
||||
dateFormat(data.dueDate),
|
||||
);
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.INVOICE_CREATION, type: NotifType.CREATE_INVOICE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async createAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ANNOUNCEMENT_MESSAGE;
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendAnnouncementSms(data.userPhone, data.title, data.description, dateFormat(data.date));
|
||||
if (data.userEmail) await this.emailService.sendAnnouncementEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ANNOUNCEMENT, type: NotifType.ANNOUNCEMENT, message, recipientId }, em);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createAnswerTicketNotification(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ANSWER_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendAnswerTicketSms(data.userPhone, data.ticketId, data.subject);
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ANSWER_TICKET, type: NotifType.ANSWER_TICKET, message, recipientId }, em);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createTicketNotification(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.CREATE_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendCreateTicketSms(data.userPhone, data.ticketId, data.subject, dateFormat(data.date));
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.CREATE_TICKET, type: NotifType.CREATE_TICKET, message, recipientId }, em);
|
||||
}
|
||||
//************************ */
|
||||
async createAssignTicketNotificationForAdmin(recipientId: string, data: ITicketNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.ASSIGN_TICKET_MESSAGE.replace("[ticketId]", data.ticketId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendTicketAssignedToAdminSms(data.userPhone, data.ticketId, data.subject, data.user!);
|
||||
if (data.userEmail) await this.emailService.sendTicketEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.ASSIGN_TICKET, type: NotifType.ASSIGN_TICKET, message, recipientId }, em);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createBillInvoiceReminderNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.BILL_INVOICE_REMINDER_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoicePaymentReminderSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.BILL_INVOICE_REMINDER, type: NotifType.BILL_INVOICE_REMINDER, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createBillInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.BILL_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoicePaidSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.paidAt!));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.BILL_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, em);
|
||||
}
|
||||
//************************ */
|
||||
|
||||
async createApprovedInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.APPROVED_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendInvoiceApprovedSms(data.userPhone, data.invoiceId, data.price, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification({ title: NotificationMessage.APPROVED_INVOICE, type: NotifType.BILL_INVOICE, message, recipientId }, em);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createInvoiceOverdueNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.INVOICE_OVERDUE_MESSAGE.replace("[invoiceNumber]", data.invoiceId).replace(
|
||||
"[lateFee]",
|
||||
numberFormat(data.lateFee || 0),
|
||||
);
|
||||
|
||||
//sent notification based on the userSettings
|
||||
await this.smsService.sendInvoiceOverdueSms(data.userPhone, data.invoiceId, data.price, data.lateFee || 0, dateFormat(data.dueDate));
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.INVOICE_OVERDUE, type: NotifType.INVOICE_OVERDUE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//************************ */
|
||||
|
||||
async createRecurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.RECURRING_INVOICE_MESSAGE.replace("[invoiceNumber]", data.invoiceId);
|
||||
//sent notification based on the userSettings
|
||||
|
||||
await this.smsService.sendRecurringInvoiceDraftSms(data.userPhone, data.invoiceId, data.price);
|
||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||
return this.createNotification(
|
||||
{ title: NotificationMessage.RECURRING_INVOICE, type: NotifType.RECURRING_INVOICE, message, recipientId },
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
// //************************ */
|
||||
|
||||
async createPaymentReminderNotification(recipientId: string, data: IPaymentNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.PAYMENT_REMINDER_MESSAGE.replace("[amount]", numberFormat(data.amount));
|
||||
|
||||
await this.smsService.sendPaymentReminderSms(data.userPhone, numberFormat(data.amount));
|
||||
if (data.userEmail) await this.emailService.sendPaymentReminderEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{
|
||||
title: NotificationMessage.PAYMENT_REMINDER,
|
||||
type: NotifType.PAYMENT_REMINDER,
|
||||
message,
|
||||
recipientId,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
// //************************ */
|
||||
|
||||
async createPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData, em: EntityManager) {
|
||||
const message = NotificationMessage.PAYMENT_CANCELLATION_MESSAGE.replace("[amount]", numberFormat(data.amount));
|
||||
|
||||
await this.smsService.sendPaymentCancellationSms(data.userPhone, numberFormat(data.amount));
|
||||
if (data.userEmail) await this.emailService.sendPaymentCancellationEmail(data);
|
||||
|
||||
return this.createNotification(
|
||||
{
|
||||
title: NotificationMessage.PAYMENT_CANCELLATION,
|
||||
type: NotifType.PAYMENT_CANCELLATION,
|
||||
message,
|
||||
recipientId,
|
||||
},
|
||||
em,
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { Processor } from "@nestjs/bullmq";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bullmq";
|
||||
|
||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
import { NotificationsService } from "../providers/notifications.service";
|
||||
|
||||
type NotificationJobData = {
|
||||
type: NotifType;
|
||||
recipientId: string;
|
||||
data:
|
||||
| ITicketNotificationData
|
||||
| IInvoiceNotificationData
|
||||
| IAnnouncementNotificationData
|
||||
| IPaymentNotificationData
|
||||
| IOtpNotificationData;
|
||||
};
|
||||
|
||||
@Processor(NOTIFICATION.QUEUE_NAME, { concurrency: 5 })
|
||||
export class NotificationProcessor extends WorkerProcessor {
|
||||
protected readonly logger = new Logger(NotificationProcessor.name);
|
||||
|
||||
constructor(
|
||||
private readonly notificationsService: NotificationsService,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<NotificationJobData>, token?: string) {
|
||||
this.logger.log(`Processing notification job: ${job.id} ${token ? `with token: ${token}` : ""}`);
|
||||
|
||||
const entityManager = this.em.fork();
|
||||
|
||||
try {
|
||||
await entityManager.begin();
|
||||
|
||||
const { type, recipientId, data } = job.data;
|
||||
|
||||
switch (type) {
|
||||
// User Notifications
|
||||
case NotifType.OTP:
|
||||
await this.notificationsService.createOtpNotification(data as IOtpNotificationData);
|
||||
break;
|
||||
|
||||
case NotifType.USER_LOGIN:
|
||||
await this.notificationsService.createLoginNotification(recipientId, data);
|
||||
break;
|
||||
case NotifType.ANNOUNCEMENT:
|
||||
await this.notificationsService.createAnnouncementNotification(recipientId, data as IAnnouncementNotificationData, entityManager);
|
||||
break;
|
||||
|
||||
// Ticket Notifications
|
||||
case NotifType.ANSWER_TICKET:
|
||||
await this.notificationsService.createAnswerTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.CREATE_TICKET:
|
||||
await this.notificationsService.createTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.ASSIGN_TICKET:
|
||||
await this.notificationsService.createAssignTicketNotificationForAdmin(
|
||||
recipientId,
|
||||
data as ITicketNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
|
||||
// Invoice Notifications
|
||||
case NotifType.CREATE_INVOICE:
|
||||
await this.notificationsService.createInvoiceCreationNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.BILL_INVOICE_REMINDER:
|
||||
await this.notificationsService.createBillInvoiceReminderNotification(
|
||||
recipientId,
|
||||
data as IInvoiceNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
case NotifType.BILL_INVOICE:
|
||||
await this.notificationsService.createBillInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.APPROVE_INVOICE:
|
||||
await this.notificationsService.createApprovedInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.INVOICE_OVERDUE:
|
||||
await this.notificationsService.createInvoiceOverdueNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.RECURRING_INVOICE:
|
||||
await this.notificationsService.createRecurringInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||
break;
|
||||
|
||||
// Payment Notifications
|
||||
case NotifType.PAYMENT_REMINDER:
|
||||
await this.notificationsService.createPaymentReminderNotification(recipientId, data as IPaymentNotificationData, entityManager);
|
||||
break;
|
||||
case NotifType.PAYMENT_CANCELLATION:
|
||||
await this.notificationsService.createPaymentCancellationNotification(
|
||||
recipientId,
|
||||
data as IPaymentNotificationData,
|
||||
entityManager,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.warn(`Unknown notification type: ${type}`);
|
||||
}
|
||||
|
||||
await entityManager.commit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to process notification: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
);
|
||||
await entityManager.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { InjectQueue } from "@nestjs/bullmq";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Queue } from "bullmq";
|
||||
|
||||
import { NOTIFICATION } from "../constants";
|
||||
import { NotifType } from "../enums/notif-settings.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IBaseNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IOtpNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../interfaces/ISendNotificationData";
|
||||
|
||||
@Injectable()
|
||||
export class NotificationQueue {
|
||||
constructor(@InjectQueue(NOTIFICATION.QUEUE_NAME) private readonly notificationsQueue: Queue) {}
|
||||
|
||||
async addNotificationJob(type: NotifType, recipientId: string, data: IBaseNotificationData) {
|
||||
await this.notificationsQueue.add(
|
||||
NOTIFICATION.SEND_NOTIFICATION_JOB_NAME,
|
||||
{
|
||||
type,
|
||||
recipientId,
|
||||
data,
|
||||
},
|
||||
{
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_DELAY,
|
||||
attempts: NOTIFICATION.SEND_NOTIFICATION_JOB_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: NOTIFICATION.SEND_NOTIFICATION_JOB_BACKOFF,
|
||||
},
|
||||
priority: NOTIFICATION.SEND_NOTIFICATION_JOB_PRIORITY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async addOtpNotification(recipientId: string, data: IOtpNotificationData) {
|
||||
await this.addNotificationJob(NotifType.OTP, recipientId, data);
|
||||
}
|
||||
|
||||
// User notification methods
|
||||
async addLoginNotification(recipientId: string, data: IBaseNotificationData) {
|
||||
await this.addNotificationJob(NotifType.USER_LOGIN, recipientId, data);
|
||||
}
|
||||
|
||||
async addInvoiceCreationNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.CREATE_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addAnnouncementNotification(recipientId: string, data: IAnnouncementNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ANNOUNCEMENT, recipientId, data);
|
||||
}
|
||||
|
||||
async addAnswerTicketNotification(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ANSWER_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addTicketNotification(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.CREATE_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addAssignTicketNotificationForAdmin(recipientId: string, data: ITicketNotificationData) {
|
||||
await this.addNotificationJob(NotifType.ASSIGN_TICKET, recipientId, data);
|
||||
}
|
||||
|
||||
async addBillInvoiceReminderNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.BILL_INVOICE_REMINDER, recipientId, data);
|
||||
}
|
||||
|
||||
async addBillInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.BILL_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addApprovedInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.APPROVE_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addInvoiceOverdueNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.INVOICE_OVERDUE, recipientId, data);
|
||||
}
|
||||
|
||||
async addRecurringInvoiceNotification(recipientId: string, data: IInvoiceNotificationData) {
|
||||
await this.addNotificationJob(NotifType.RECURRING_INVOICE, recipientId, data);
|
||||
}
|
||||
|
||||
async addPaymentReminderNotification(recipientId: string, data: IPaymentNotificationData) {
|
||||
await this.addNotificationJob(NotifType.PAYMENT_REMINDER, recipientId, data);
|
||||
}
|
||||
|
||||
async addPaymentCancellationNotification(recipientId: string, data: IPaymentNotificationData) {
|
||||
await this.addNotificationJob(NotifType.PAYMENT_CANCELLATION, recipientId, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||
import { Notification } from "../entities/notification.entity";
|
||||
|
||||
export class NotificationRepository extends EntityRepository<Notification> {
|
||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||
const { skip, limit } = PaginationUtils(queryDto);
|
||||
|
||||
const qb = this.qb("notification").where({ recipientId });
|
||||
|
||||
if (queryDto.isRead !== undefined) {
|
||||
qb.andWhere({ isRead: queryDto.isRead === 1 });
|
||||
}
|
||||
|
||||
return qb.orderBy({ createdAt: "DESC" }).offset(skip).limit(limit).getResultAndCount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Entity, ManyToOne, Property } from "@mikro-orm/core";
|
||||
|
||||
import { User } from "./user.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
|
||||
@Entity()
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 255 })
|
||||
token!: string;
|
||||
|
||||
@ManyToOne(() => User, { deleteRule: "restrict" })
|
||||
user!: User;
|
||||
|
||||
@Property({ type: "timestamptz", nullable: true })
|
||||
expiresAt?: Date;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Collection, Entity, Enum, OneToMany } from "@mikro-orm/core";
|
||||
|
||||
import { User } from "./user.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { RoleEnum } from "../enums/role.enum";
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
@Enum({ items: () => RoleEnum, nativeEnumName: "role_enum", default: RoleEnum.USER })
|
||||
name!: RoleEnum;
|
||||
|
||||
@OneToMany(() => User, (user) => user.role)
|
||||
users = new Collection<User>(this);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Opt, Property } from "@mikro-orm/core";
|
||||
|
||||
import { RefreshToken } from "./refresh-token.entity";
|
||||
import { Role } from "./role.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
|
||||
@Entity({ repository: () => UserRepository })
|
||||
export class User extends BaseEntity {
|
||||
@Property({ type: "varchar", length: 150, nullable: true })
|
||||
email?: string;
|
||||
|
||||
@Property({ type: "varchar", length: 11, unique: true, nullable: false })
|
||||
phone!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 50, unique: true, nullable: true })
|
||||
userName!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 150 })
|
||||
password!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 150 })
|
||||
firstName!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 200 })
|
||||
lastName!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 12, nullable: true })
|
||||
birthDate?: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100, unique: true, nullable: true })
|
||||
nationalCode?: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100, nullable: true })
|
||||
profilePic?: string;
|
||||
|
||||
@Property({ type: "boolean", default: false })
|
||||
emailVerified!: boolean & Opt;
|
||||
|
||||
//-----------------------------------
|
||||
|
||||
@ManyToOne(() => Role, { deleteRule: "restrict" })
|
||||
role!: Role;
|
||||
|
||||
//-----------------------------------
|
||||
|
||||
@OneToMany(() => RefreshToken, (token) => token.user)
|
||||
refreshTokens = new Collection<RefreshToken>(this);
|
||||
|
||||
[EntityRepositoryType]?: UserRepository;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum RoleEnum {
|
||||
ADMIN = "ADMIN",
|
||||
USER = "USER",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository } from "@mikro-orm/core";
|
||||
|
||||
import { User } from "../entities/user.entity";
|
||||
|
||||
export class UserRepository extends EntityRepository<User> {}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { EntityManager } from "@mikro-orm/postgresql";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import slugify from "slugify";
|
||||
|
||||
import { UserMessage } from "../../../common/enums/message.enum";
|
||||
import { CompleteRegistrationDto } from "../../auth/DTO/complete-register.dto";
|
||||
import { Role } from "../entities/role.entity";
|
||||
import { User } from "../entities/user.entity";
|
||||
import { RoleEnum } from "../enums/role.enum";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly em: EntityManager,
|
||||
) {}
|
||||
|
||||
/*******************************/
|
||||
|
||||
async findOneWithEmail(email: string) {
|
||||
const user = await this.userRepository.findOne({ email }, { populate: ["role"] });
|
||||
return user;
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async findOneWithPhone(phone: string) {
|
||||
const user = await this.userRepository.findOne({ phone }, { populate: ["role"] });
|
||||
return user;
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async findOneById(id: string) {
|
||||
const user = await this.userRepository.findOne({ id }, { populate: ["role"] });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { user };
|
||||
}
|
||||
/*******************************/
|
||||
|
||||
async getMe(userId: string) {
|
||||
const user = await this.userRepository.findOne({ id: userId }, { populate: ["role"] });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
return { user };
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
|
||||
async createUser(registerDto: CompleteRegistrationDto, hashedPassword: string, em: EntityManager) {
|
||||
const role = await em.findOne(Role, { name: RoleEnum.USER });
|
||||
if (!role) throw new BadRequestException(UserMessage.ROLE_NOT_FOUND);
|
||||
|
||||
const existPhone = await em.findOne(User, { phone: registerDto.phone });
|
||||
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
|
||||
|
||||
const existUser = await em.findOne(User, { nationalCode: registerDto.nationalCode });
|
||||
if (existUser) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST);
|
||||
|
||||
const userName = slugify(`${registerDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true });
|
||||
|
||||
const user = em.create(User, {
|
||||
...registerDto,
|
||||
userName,
|
||||
password: hashedPassword,
|
||||
role,
|
||||
});
|
||||
|
||||
await em.persistAndFlush(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
|
||||
async updateUserPassword(user: User, newPassword: string) {
|
||||
user.password = newPassword;
|
||||
await this.em.flush();
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { UsersService } from "./services/users.service";
|
||||
import { UserDec } from "../../common/decorators/user.decorator";
|
||||
|
||||
@Controller("users")
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@ApiOperation({ summary: "Get user profile" })
|
||||
@Get("me")
|
||||
getMe(@UserDec("id") userId: string) {
|
||||
return this.usersService.getMe(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { User } from "./entities/user.entity";
|
||||
import { UsersService } from "./services/users.service";
|
||||
import { UsersController } from "./users.controller";
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([User])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
export const SMS_CONFIG = "SMS_CONFIG";
|
||||
export const S3_CONFIG = "S3_CONFIG";
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
export interface IFile {
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
buffer: Buffer;
|
||||
size: number;
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY";
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
export interface IParameterArray {
|
||||
name: TemplateParams;
|
||||
value: string;
|
||||
}
|
||||
export interface ISmsResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
//----------------------------------------------
|
||||
|
||||
export interface ISmsGetLineResponse extends ISmsResponse {
|
||||
data: string[];
|
||||
}
|
||||
|
||||
export interface ISmsVerifyResponse extends ISmsResponse {
|
||||
data: { MessageId: number; Cost: number };
|
||||
}
|
||||
|
||||
//----------------------------------------------
|
||||
export interface ISmsVerifyBody {
|
||||
Parameters: IParameterArray[];
|
||||
Mobile: string;
|
||||
TemplateId: string;
|
||||
}
|
||||
|
||||
export type TemplateParams =
|
||||
| "VERIFICATIONCODE"
|
||||
| "code"
|
||||
| "invoiceId"
|
||||
| "price"
|
||||
| "items"
|
||||
| "loginDate"
|
||||
| "invoiceDate"
|
||||
| "title"
|
||||
| "description"
|
||||
| "ticketId"
|
||||
| "serviceName"
|
||||
| "date"
|
||||
| "amount"
|
||||
| "newBalance"
|
||||
| "reason"
|
||||
| "dueDate"
|
||||
| "lateFee"
|
||||
| "paidDate"
|
||||
| "user"
|
||||
| "subject"
|
||||
| "planName"
|
||||
| "message"
|
||||
| "blogTitle"
|
||||
| "fullName"
|
||||
| "mobile";
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
import { CACHE_MANAGER, Cache } from "@nestjs/cache-manager";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
async get<T>(key: string) {
|
||||
return await this.cacheManager.get<T>(key);
|
||||
}
|
||||
|
||||
async set(key: string, value: string, ttl: number = 120 * 1000) {
|
||||
await this.cacheManager.set(key, value, ttl);
|
||||
}
|
||||
|
||||
async del(key: string) {
|
||||
await this.cacheManager.del(key);
|
||||
}
|
||||
|
||||
async getTtl(key: string) {
|
||||
return await this.cacheManager.ttl(key);
|
||||
}
|
||||
}
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { MailerService } from "@nestjs-modules/mailer";
|
||||
import { SentMessageInfo } from "nodemailer";
|
||||
|
||||
import { dateFormat, numberFormat } from "./international.utils";
|
||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||
import {
|
||||
IAnnouncementNotificationData,
|
||||
IInvoiceNotificationData,
|
||||
IPaymentNotificationData,
|
||||
ITicketNotificationData,
|
||||
} from "../../notifications/interfaces/ISendNotificationData";
|
||||
import { IBaseNotificationData } from "../../notifications/interfaces/ISendNotificationData";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
private readonly logger = new Logger(EmailService.name);
|
||||
constructor(private readonly mailerService: MailerService) {}
|
||||
|
||||
async sendVerificationEmail(to: string, link: string, fullName: string): Promise<SentMessageInfo> {
|
||||
try {
|
||||
const emailInfo = await this.mailerService.sendMail({
|
||||
to: to,
|
||||
|
||||
subject: EmailMessage.EMAIL_VERIFICATION,
|
||||
template: "email-verify",
|
||||
context: {
|
||||
title: EmailMessage.EMAIL_VERIFICATION,
|
||||
link,
|
||||
fullName,
|
||||
},
|
||||
});
|
||||
return emailInfo;
|
||||
} catch (error) {
|
||||
this.logger.error("Email sending failed:", error);
|
||||
throw new InternalServerErrorException(EmailMessage.EMAIL_SENDING_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
async sendLoginEmail(data: IBaseNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.LOGIN,
|
||||
template: "login",
|
||||
context: {
|
||||
date: dateFormat(new Date()),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending login email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendInvoiceEmail(data: IInvoiceNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.INVOICE,
|
||||
template: "invoice",
|
||||
context: {
|
||||
price: numberFormat(data.price),
|
||||
createDate: dateFormat(data.createDate),
|
||||
dueDate: dateFormat(data.dueDate),
|
||||
invoiceId: data.invoiceId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending invoice email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendTicketEmail(data: ITicketNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.TICKET,
|
||||
template: "ticket",
|
||||
context: {
|
||||
ticketId: data.ticketId,
|
||||
subject: data.subject,
|
||||
date: data.date ? dateFormat(data.date) : dateFormat(new Date()),
|
||||
user: data.user,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending ticket email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendAnnouncementEmail(data: IAnnouncementNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.ANNOUNCEMENT,
|
||||
template: "announcement",
|
||||
context: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
date: dateFormat(data.date),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending announcement email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPaymentReminderEmail(data: IPaymentNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.PAYMENT_REMINDER,
|
||||
template: "payment-reminder",
|
||||
context: {
|
||||
amount: numberFormat(data.amount),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending payment reminder email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPaymentCancellationEmail(data: IPaymentNotificationData) {
|
||||
try {
|
||||
await this.mailerService.sendMail({
|
||||
to: data.userEmail,
|
||||
subject: EmailMessage.PAYMENT_CANCELLATION,
|
||||
template: "payment-cancellation",
|
||||
context: {
|
||||
amount: numberFormat(data.amount),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending payment cancellation email", error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendAdminNotificationEmail(data: { userEmail: string; title: string; message: string }) {
|
||||
const emailData = {
|
||||
to: data.userEmail,
|
||||
subject: data.title,
|
||||
template: "admin-notification",
|
||||
context: {
|
||||
title: data.title,
|
||||
message: data.message,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await this.mailerService.sendMail(emailData);
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending admin notification email", error);
|
||||
// throw new InternalServerErrorException("error in sending admin notification email");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export function numberFormat(number: number, locale: string = "fa-IR") {
|
||||
return Intl.NumberFormat(locale).format(number);
|
||||
}
|
||||
|
||||
export function dateFormat(date: Date, locale: string = "fa-IR") {
|
||||
return new Intl.DateTimeFormat(locale).format(date);
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
import { randomInt } from "node:crypto";
|
||||
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { CacheService } from "./cache.service";
|
||||
import { OtpCacheKeyType } from "../interfaces/IOtpKey";
|
||||
|
||||
@Injectable()
|
||||
export class OTPService {
|
||||
constructor(private cacheService: CacheService) {}
|
||||
//
|
||||
private generateOTP(): string {
|
||||
return randomInt(10000, 99999).toString();
|
||||
}
|
||||
//
|
||||
async generateAndSetInCache(identifier: string, cacheKey: OtpCacheKeyType) {
|
||||
const otp = this.generateOTP();
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
await this.cacheService.set(key, otp, 180 * 1000);
|
||||
return otp;
|
||||
}
|
||||
//
|
||||
async verifyOtp(identifier: string, otpCode: string, cacheKey: OtpCacheKeyType) {
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
const codeInCache = await this.cacheService.get<string | null>(key);
|
||||
return codeInCache && otpCode === codeInCache;
|
||||
}
|
||||
//
|
||||
async delOtpFormCache(identifier: string, cacheKey: OtpCacheKeyType) {
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
await this.cacheService.del(key);
|
||||
}
|
||||
|
||||
async checkExistOtp(identifier: string, cacheKey: OtpCacheKeyType) {
|
||||
const key = `${identifier}:${cacheKey}:OTP`;
|
||||
const codeInCache = await this.cacheService.get<string | null>(key);
|
||||
|
||||
if (!codeInCache) return null;
|
||||
|
||||
const ttlInMsSeconds = await this.cacheService.getTtl(key);
|
||||
|
||||
return Math.floor((ttlInMsSeconds! - Date.now()) / 1000);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
|
||||
export function PaginationUtils(paginationData: PaginationDto) {
|
||||
const { limit = 10, page = 1 } = paginationData;
|
||||
|
||||
// const pageN = Number.parseInt(page) || 1;
|
||||
// const limitN = Number.parseInt(limit) || 10;
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
return {
|
||||
// page: page,
|
||||
// limit: limit > 50 ? 50 : limit,
|
||||
limit: limit,
|
||||
skip: skip,
|
||||
};
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { compare, genSalt, hash } from "bcrypt";
|
||||
@Injectable()
|
||||
export class PasswordService {
|
||||
//** */
|
||||
async hashPassword(rawPassword: string) {
|
||||
const salt = await genSalt(10);
|
||||
return hash(rawPassword, salt);
|
||||
}
|
||||
//** */
|
||||
async comparePassword(rawPassword: string, hashedPassword: string) {
|
||||
return compare(rawPassword, hashedPassword);
|
||||
}
|
||||
}
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { extname } from "node:path";
|
||||
|
||||
import { PutObjectCommand, PutObjectCommandInput, S3Client } from "@aws-sdk/client-s3";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
|
||||
import { IS3Configs } from "../../../configs/s3.config";
|
||||
import { S3_CONFIG } from "../constants";
|
||||
import { IFile } from "../interfaces/IFile";
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
private readonly logger = new Logger(S3Service.name);
|
||||
private client: S3Client;
|
||||
|
||||
//
|
||||
constructor(@Inject(S3_CONFIG) private s3Configs: IS3Configs) {
|
||||
this.client = new S3Client({
|
||||
endpoint: this.s3Configs.endpoint,
|
||||
region: this.s3Configs.region,
|
||||
credentials: {
|
||||
accessKeyId: this.s3Configs.accessKeyId,
|
||||
secretAccessKey: this.s3Configs.secretAccessKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async upload(file: IFile) {
|
||||
const extName = extname(file.originalname);
|
||||
const params: PutObjectCommandInput = {
|
||||
Body: file.buffer,
|
||||
Bucket: this.s3Configs.bucket,
|
||||
Key: `images/${randomUUID()}${Date.now()}${extName}`,
|
||||
ACL: "public-read",
|
||||
};
|
||||
//
|
||||
|
||||
try {
|
||||
await this.client.send(new PutObjectCommand(params));
|
||||
const url = `${this.s3Configs.url}/${params.Key}`;
|
||||
return {
|
||||
url,
|
||||
originalname: file.originalname,
|
||||
size: file.size,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
throw new InternalServerErrorException("Failed to upload file");
|
||||
}
|
||||
}
|
||||
|
||||
async uploadMultiple(files: IFile[]) {
|
||||
const uploadPromises = files.map((file) => this.upload(file));
|
||||
const results = await Promise.all(uploadPromises);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
Executable
+543
@@ -0,0 +1,543 @@
|
||||
import { HttpService } from "@nestjs/axios";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { AxiosError } from "axios";
|
||||
import { catchError, firstValueFrom } from "rxjs";
|
||||
|
||||
import { SmsMessage } from "../../../common/enums/message.enum";
|
||||
import { ISmsConfigs } from "../../../configs/sms.config";
|
||||
import { SMS_CONFIG } from "../constants";
|
||||
import { ISmsGetLineResponse, ISmsVerifyBody, ISmsVerifyResponse } from "../interfaces/ISms";
|
||||
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
private readonly logger = new Logger(SmsService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(SMS_CONFIG) private smsConfigs: ISmsConfigs,
|
||||
private readonly httpService: HttpService,
|
||||
) {}
|
||||
//************************************************* */
|
||||
|
||||
async sendSmsVerifyCode(mobile: string, otpCode: string) {
|
||||
//
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "code", value: otpCode }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_OTP,
|
||||
};
|
||||
//
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(
|
||||
`${this.smsConfigs.API_URL}/send/verify`,
|
||||
{ ...smsData },
|
||||
{
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
},
|
||||
)
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending verify sms", err);
|
||||
throw new InternalServerErrorException("error in sending verify sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
console.log(data, "================");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoiceVerifyCode(mobile: string, otpCode: string, invoiceId: number, price: number, items: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "code", value: otpCode },
|
||||
{ name: "invoiceId", value: invoiceId.toString() },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
{ name: "items", value: items },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE,
|
||||
};
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(
|
||||
`${this.smsConfigs.API_URL}/send/verify`,
|
||||
{ ...smsData },
|
||||
{
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
},
|
||||
)
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending verify sms", err);
|
||||
throw new InternalServerErrorException("error in sending verify sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
throw new InternalServerErrorException(SmsMessage.SEND_SMS_ERROR);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendLoginSms(mobile: string, loginDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "loginDate", value: loginDate }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_LOGIN,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending verify sms", err);
|
||||
throw new InternalServerErrorException("error in sending verify sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
{ name: "items", value: items },
|
||||
{ name: "invoiceDate", value: invoiceDate },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_CREATED,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
console.error(err);
|
||||
|
||||
this.logger.error("error in sending invoice created sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice created sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
async sendAnnouncementSms(mobile: string, title: string, description: string, date: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "title", value: title },
|
||||
{ name: "description", value: description },
|
||||
{ name: "date", value: date },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_ANNOUNCEMENT,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending announcement sms", err);
|
||||
throw new InternalServerErrorException("error in sending announcement sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendCreateTicketSms(mobile: string, ticketId: string, subject: string, date: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "ticketId", value: ticketId },
|
||||
{ name: "subject", value: subject },
|
||||
{ name: "date", value: date },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_CREATED,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending ticket created sms", err);
|
||||
throw new InternalServerErrorException("error in sending ticket created sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendAnswerTicketSms(mobile: string, ticketId: string, subject: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "ticketId", value: ticketId },
|
||||
{ name: "subject", value: subject },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ANSWERED,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending ticket answered sms", err);
|
||||
throw new InternalServerErrorException("error in sending ticket answered sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendTicketAssignedToAdminSms(mobile: string, ticketId: string, subject: string, user: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "ticketId", value: ticketId },
|
||||
{ name: "subject", value: subject },
|
||||
{ name: "user", value: user },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_TICKET_ASSIGNED_ADMIN,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending ticket assigned sms", err);
|
||||
throw new InternalServerErrorException("error in sending ticket assigned sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
async sendInvoiceApprovedSms(mobile: string, invoiceId: string, price: number, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_APPROVED,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending invoice approved sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice approved sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
async sendInvoicePaidSms(mobile: string, invoiceId: string, amount: number, paidDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "amount", value: amount.toString() },
|
||||
{ name: "paidDate", value: paidDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_PAID,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending invoice paid sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice paid sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoicePaymentReminderSms(mobile: string, invoiceId: string, amount: number, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "amount", value: amount.toString() },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_REMINDER,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending invoice reminder sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice reminder sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendInvoiceOverdueSms(mobile: string, invoiceId: string, amount: number, lateFee: number, dueDate: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "amount", value: amount.toString() },
|
||||
{ name: "lateFee", value: lateFee.toString() },
|
||||
{ name: "dueDate", value: dueDate },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_OVERDUE,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending invoice overdue sms", err);
|
||||
throw new InternalServerErrorException("error in sending invoice overdue sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendRecurringInvoiceDraftSms(adminMobile: string, invoiceId: string, price: number) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "price", value: Intl.NumberFormat("fa-IR").format(price) },
|
||||
],
|
||||
Mobile: adminMobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_RECURRING_INVOICE_DRAFT,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending recurring invoice draft sms", err);
|
||||
throw new InternalServerErrorException("error in sending recurring invoice draft sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
|
||||
async sendBlockServiceSms(mobile: string, invoiceId: string, planName: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [
|
||||
{ name: "invoiceId", value: invoiceId },
|
||||
{ name: "planName", value: planName },
|
||||
],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_SUBSCRIPTION_CANCELLED,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending subscription cancelled sms", err);
|
||||
throw new InternalServerErrorException("error in sending subscription cancelled sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
// throw new InternalServerErrorException("error in sending sms");
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
//************************************************* */
|
||||
|
||||
async sendPaymentReminderSms(mobile: string, amount: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "amount", value: amount }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_REMINDER,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending payment reminder sms", err);
|
||||
throw new InternalServerErrorException("error in sending payment reminder sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
async sendPaymentCancellationSms(mobile: string, amount: string) {
|
||||
const smsData: ISmsVerifyBody = {
|
||||
Parameters: [{ name: "amount", value: amount }],
|
||||
Mobile: mobile,
|
||||
TemplateId: this.smsConfigs.SMS_PATTERN_PAYMENT_CANCELLATION,
|
||||
};
|
||||
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.post<ISmsVerifyResponse>(`${this.smsConfigs.API_URL}/send/verify`, smsData, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((err: AxiosError) => {
|
||||
this.logger.error("error in sending payment cancellation sms", err);
|
||||
throw new InternalServerErrorException("error in sending payment cancellation sms");
|
||||
}),
|
||||
),
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in sending sms", error);
|
||||
}
|
||||
}
|
||||
|
||||
async getSmsLines() {
|
||||
try {
|
||||
const { data } = await firstValueFrom(
|
||||
this.httpService
|
||||
.get<ISmsGetLineResponse>(`${this.smsConfigs.API_URL}/lines`, {
|
||||
headers: { "X-API-KEY": this.smsConfigs.API_KEY },
|
||||
})
|
||||
.pipe(
|
||||
catchError((error: AxiosError) => {
|
||||
this.logger.error("error in getting sms lines", error);
|
||||
throw new InternalServerErrorException("error in getting sms lines");
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error("error in getting sms lines", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { S3_CONFIG, SMS_CONFIG } from "./constants";
|
||||
import { CacheService } from "./providers/cache.service";
|
||||
import { EmailService } from "./providers/email.service";
|
||||
import { OTPService } from "./providers/otp.service";
|
||||
import { PasswordService } from "./providers/password.service";
|
||||
import { S3Service } from "./providers/s3.service";
|
||||
import { SmsService } from "./providers/sms.service";
|
||||
import { S3Configs } from "../../configs/s3.config";
|
||||
import { smsConfigs } from "../../configs/sms.config";
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
EmailService,
|
||||
OTPService,
|
||||
PasswordService,
|
||||
CacheService,
|
||||
SmsService,
|
||||
S3Service,
|
||||
{
|
||||
provide: SMS_CONFIG,
|
||||
useFactory: smsConfigs().useFactory,
|
||||
inject: smsConfigs().inject,
|
||||
},
|
||||
{
|
||||
provide: S3_CONFIG,
|
||||
useFactory: S3Configs().useFactory,
|
||||
inject: S3Configs().inject,
|
||||
},
|
||||
],
|
||||
exports: [SMS_CONFIG, S3_CONFIG, S3Service, OTPService, PasswordService, CacheService, SmsService, EmailService],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { App } from "supertest/types";
|
||||
|
||||
import { AppModule } from "./../src/app.module";
|
||||
|
||||
describe("AppController (e2e)", () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("/ (GET)", () => {
|
||||
return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user