chore: first

This commit is contained in:
Mahyargdz
2025-05-11 10:27:30 +03:30
commit 4e563c497d
102 changed files with 18602 additions and 0 deletions
+47
View File
@@ -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
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
npx --no-install commitlint --edit "$1"
+1
View File
@@ -0,0 +1 @@
pnpm dlx lint-staged
+9
View File
@@ -0,0 +1,9 @@
{
"singleQuote": false,
"trailingComma": "all",
"endOfLine": "auto",
"printWidth": 140,
"arrowParens": "always",
"semi": true,
"tabWidth": 2
}
Executable
+41
View File
@@ -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"]
+98
View File
@@ -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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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).
+8
View File
@@ -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"]],
},
};
+36
View File
@@ -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;
+20
View File
@@ -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:
+130
View File
@@ -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",
},
},
);
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+125
View File
@@ -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"
}
+13911
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -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("*");
}
}
+22
View File
@@ -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;
}
+19
View File
@@ -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;
}
+6
View File
@@ -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";
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+4
View File
@@ -0,0 +1,4 @@
// import { SetMetadata } from "@nestjs/common";
// export const ROLES_KEY = "roles";
// export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
+5
View File
@@ -0,0 +1,5 @@
import { SetMetadata } from "@nestjs/common";
export const SKIP_AUTH_KEY = "skipAuth";
export const SkipAuth = () => SetMetadata(SKIP_AUTH_KEY, true);
+23
View File
@@ -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;
// });
+16
View File
@@ -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;
}
+525
View File
@@ -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 = "خطا در ارسال پیامک",
}
+9
View File
@@ -0,0 +1,9 @@
export interface ErrorResponse {
statusCode: number;
success: boolean;
error: {
message: string | string[];
timestamp?: string;
path?: string;
};
}
+19
View File
@@ -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;
}
+9
View File
@@ -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"),
};
+33
View File
@@ -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}.`);
}
}
+19
View File
@@ -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,
},
}),
};
}
+16
View File
@@ -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:" })],
};
},
};
}
+15
View File
@@ -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,
};
},
};
}
+16
View File
@@ -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"),
},
};
},
};
}
+31
View File
@@ -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,
},
},
}),
};
}
+54
View File
@@ -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",
},
}),
};
+21
View File
@@ -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")),
}),
};
}
+26
View File
@@ -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;
}
+49
View File
@@ -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;
}
+24
View File
@@ -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);
}
+20
View File
@@ -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;
}
+47
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
}),
);
}
}
+30
View File
@@ -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
View File
@@ -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();
+24
View File
@@ -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
View File
@@ -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;
}
+18
View File
@@ -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) {}
+9
View File
@@ -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;
}
+12
View File
@@ -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;
}
+20
View File
@@ -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"]) {}
+98
View File
@@ -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);
// }
}
+20
View File
@@ -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 {}
+25
View File
@@ -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;
}
}
+28
View File
@@ -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;
}
}
+32
View File
@@ -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;
// }
// }
+27
View File
@@ -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
View File
@@ -0,0 +1,6 @@
import { RoleEnum } from "../../users/enums/role.enum";
export interface ITokenPayload {
id: string;
role: RoleEnum;
}
+227
View File
@@ -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;
}
}
+143
View File
@@ -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");
}
}
+23
View File
@@ -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 };
}
}
+23
View File
@@ -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
});
+26
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+14
View File
@@ -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);
}
+51
View File
@@ -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;
}
+4
View File
@@ -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;
}
}
+16
View File
@@ -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);
}
}
+14
View File
@@ -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 {}
+2
View File
@@ -0,0 +1,2 @@
export const SMS_CONFIG = "SMS_CONFIG";
export const S3_CONFIG = "S3_CONFIG";
+8
View File
@@ -0,0 +1,8 @@
export interface IFile {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
buffer: Buffer;
size: number;
}
+1
View File
@@ -0,0 +1 @@
export type OtpCacheKeyType = "LOGIN" | "REGISTER" | "FORGOT_PASSWORD" | "INVOICE_VERIFY";
+51
View File
@@ -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";
+23
View File
@@ -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);
}
}
View File
+156
View File
@@ -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
View File
@@ -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);
}
+44
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+57
View File
@@ -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;
}
}
+543
View File
@@ -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);
}
}
}
+34
View File
@@ -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 {}
+23
View File
@@ -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!");
});
});
+9
View File
@@ -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