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