init
This commit is contained in:
+56
@@ -0,0 +1,56 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"insertPragma": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"eslint.format.enable": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit",
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
# Stage 1: Base
|
||||||
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
|
# Install timezone support
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||||
|
|
||||||
|
# Set workdir for clarity
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Stage 2: Dependencies
|
||||||
|
FROM base AS deps
|
||||||
|
WORKDIR /temp-deps
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
|
# Stage 3: Build
|
||||||
|
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 npm run build; fi
|
||||||
|
|
||||||
|
# Stage 4: Runner
|
||||||
|
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 3000
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
|
||||||
|
# # 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 3000
|
||||||
|
|
||||||
|
# CMD ["npm", "start"]
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g @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).
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
'@typescript-eslint/consistent-type-imports': 'error',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+13998
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"name": "base-api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"db:create": "npx mikro-orm schema:create --run --config ./src/config/mikro-orm.config.dev.ts",
|
||||||
|
"db:update": "npx mikro-orm schema:update --run --config ./src/config/mikro-orm.config.dev.ts",
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start dist/main",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node --max-old-space-size=2048 dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.922.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.922.0",
|
||||||
|
"@fastify/multipart": "^9.3.0",
|
||||||
|
"@fastify/static": "^8.3.0",
|
||||||
|
"@keyv/redis": "^5.1.3",
|
||||||
|
"@mikro-orm/core": "^6.5.8",
|
||||||
|
"@mikro-orm/nestjs": "^6.1.1",
|
||||||
|
"@mikro-orm/postgresql": "^6.5.8",
|
||||||
|
"@nest-lab/fastify-multer": "^1.3.0",
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
|
"@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.1",
|
||||||
|
"@nestjs/mapped-types": "*",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/platform-fastify": "^11.1.8",
|
||||||
|
"@nestjs/swagger": "^11.2.1",
|
||||||
|
"@nestjs/throttler": "^6.4.0",
|
||||||
|
"axios": "^1.13.1",
|
||||||
|
"cache-manager": "^7.2.4",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
|
"fastify": "^5.6.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@mikro-orm/cli": "^6.5.8",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+14002
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import dataBaseConfig from './config/mikro-orm.config';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { UserModule } from './modules/user/user.module';
|
||||||
|
// import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
// import { cacheConfig } from './config/cache.config';
|
||||||
|
import { UtilsModule } from './modules/utils/utils.module';
|
||||||
|
// import { HttpModule } from '@nestjs/axios';
|
||||||
|
// import { httpConfig } from './config/http.config';
|
||||||
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { UploaderModule } from './modules/uploader/uploader.module';
|
||||||
|
import { AdminModule } from './modules/admin/admin.module';
|
||||||
|
// import { CacheService } from './modules/utils/cache.service';
|
||||||
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { SubmitionModule } from './modules/submition/submition.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({ isGlobal: true, cache: true }),
|
||||||
|
MikroOrmModule.forRootAsync(dataBaseConfig),
|
||||||
|
// CacheModule.registerAsync(cacheConfig()),
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
global: true,
|
||||||
|
secret: configService.getOrThrow<string>('JWT_SECRET'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.getOrThrow<number>('JWT_EXPIRATION_TIME'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
UserModule,
|
||||||
|
UtilsModule,
|
||||||
|
AuthModule,
|
||||||
|
UploaderModule,
|
||||||
|
AdminModule,
|
||||||
|
ThrottlerModule.forRoot([
|
||||||
|
{
|
||||||
|
ttl: 60, // time window in seconds
|
||||||
|
limit: 5, // max requests per window
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
SubmitionModule
|
||||||
|
],
|
||||||
|
controllers: [],
|
||||||
|
// providers: [CacheService],
|
||||||
|
// exports: [CacheService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
||||||
|
export const AI_CONFIG = 'AI_CONFIG';
|
||||||
|
export const MIKRO_ORM_QUERY_LOGGER = 'MIKRO_ORM_QUERY_LOGGER';
|
||||||
|
export const NAJVA_CONFIG = 'NAJVA_CONFIG';
|
||||||
|
export const AUTH_THROTTLE_TTL = 1 * 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 CONSOLE_JWT_STRATEGY_NAME = 'console_jwt_strategy';
|
||||||
|
export const LOCAL_JWT_STRATEGY_NAME = 'local_jwt_strategy';
|
||||||
|
|
||||||
|
export const DANAK_IMAPS_SERVER = 'imaps.danakcorp.com';
|
||||||
|
export const DANAK_SMTPS_SERVER = 'smtps.danakcorp.com';
|
||||||
|
export const DANAK_POP3_SERVER = 'pop3.danakcorp.com';
|
||||||
|
|
||||||
|
export const DANAK_IMAPS_PORT = 993;
|
||||||
|
export const DANAK_POP3_PORT = 995;
|
||||||
|
export const DANAK_SMTPS_PORT = 465;
|
||||||
|
export const DANAK_IMAPS_ENCRYPTION = 'TLS /SSL';
|
||||||
|
export const DANAK_SMTPS_ENCRYPTION = 'TLS /SSL';
|
||||||
|
export const DANAK_POP3_ENCRYPTION = 'TLS /SSL';
|
||||||
Executable
+612
@@ -0,0 +1,612 @@
|
|||||||
|
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 = "دسترسی شما محدود شده است",
|
||||||
|
USER_ACCOUNT_INACTIVE = "حساب کاربری شما غیر فعال است",
|
||||||
|
TOKEN_MISSED_OR_EXPIRED = "توکن مفقود یا منقضی شده است",
|
||||||
|
OLD_PASSWORD_REQUIRED = "رمز عبور قبلی الزامی است",
|
||||||
|
PASSWORD_CHANGE_FAILED = "تغییر رمز عبور با مشکل روبرد شد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum UserMessage {
|
||||||
|
USER_NOT_FOUND = "کاربری با این مشخصات یافت نشد",
|
||||||
|
USER_EXISTS = "با این شماره قبلا ثبت نام شده است",
|
||||||
|
USER_REGISTER_SUCCESS = "ثبت نام موفقیت آمیز بود",
|
||||||
|
EMAIL_ADDRESS_ALREADY_EXISTS = "ایمیل قبلا ثبت شده است",
|
||||||
|
EMAIL_USER_NOT_FOUND = "ایمیل یافت نشد",
|
||||||
|
EMAIL_USER_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد",
|
||||||
|
FAILED_TO_DELETE_EMAIL_USER = "خطا در حذف ایمیل",
|
||||||
|
EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY = "ایمیل با موفقیت به روز رسانی شد",
|
||||||
|
FAILED_TO_UPDATE_EMAIL_USER_QUOTA = "خطا در به روز رسانی حجم ایمیل",
|
||||||
|
USERNAME_REQUIRED = "نام کاربری الزامی است",
|
||||||
|
USERNAME_STRING = "نام کاربری باید یک رشته باشد",
|
||||||
|
USERNAME_MIN_LENGTH = "نام کاربری باید حداقل ۲ کاراکتر باشد",
|
||||||
|
USERNAME_MATCHES = "نام کاربری فقط میتواند شامل حروف، اعداد، نقطه، خط زیر و خط فاصله باشد",
|
||||||
|
PASSWORD_MATCHES = "رمز عبور باید حداقل ۸ کاراکتر باشد و شامل حروف بزرگ، حروف کوچک، اعداد و علائم خاص باشد",
|
||||||
|
PASSWORD_STRING = "رمز عبور باید یک رشته باشد",
|
||||||
|
PASSWORD_REQUIRED = "رمز عبور الزامی است",
|
||||||
|
PASSWORD_MIN_LENGTH = "رمز عبور باید حداقل ۸ کاراکتر باشد",
|
||||||
|
DOMAIN_ID_STRING = "شناسه دامنه باید یک رشته باشد",
|
||||||
|
DOMAIN_ID_REQUIRED = "شناسه دامنه الزامی است",
|
||||||
|
DOMAIN_ID_UUID = "شناسه دامنه باید یک یو یو آی دی باشد",
|
||||||
|
DISPLAY_NAME_STRING = "نام نمایشی باید یک رشته باشد",
|
||||||
|
DISPLAY_NAME_OPTIONAL = "نام نمایشی اختیاری است",
|
||||||
|
QUOTA_NUMBER = "حجم ایمیل باید یک عدد باشد",
|
||||||
|
QUOTA_OPTIONAL = "حجم ایمیل اختیاری است",
|
||||||
|
QUOTA_NOT_EMPTY = "حجم ایمیل نمیتواند خالی باشد",
|
||||||
|
ALIASES_ARRAY = "الیاس ها باید یک آرایه باشد",
|
||||||
|
ALIASES_STRING = "الیاس ها باید یک رشته باشد",
|
||||||
|
ALIASES_OPTIONAL = "الیاس ها اختیاری است",
|
||||||
|
TITLE_STRING = "عنوان باید یک رشته باشد",
|
||||||
|
TITLE_OPTIONAL = "عنوان اختیاری است",
|
||||||
|
TITLE_NOT_EMPTY = "عنوان نمیتواند خالی باشد",
|
||||||
|
EMAIL_USER_CREATED_SUCCESSFULLY = "ایمیل با موفقیت ایجاد شد",
|
||||||
|
EMAIL_USER_UPDATED_SUCCESSFULLY = "ایمیل کاربر با موفقیت بهروزرسانی شد",
|
||||||
|
STATUS_UPDATED = "وضعیت کاربر با موفقیت تغییر کرد",
|
||||||
|
TEMPLATE_UUID = "شناسه قالب باید یک یو یو آی دی باشد",
|
||||||
|
TEMPLATE_NOT_EMPTY = "شناسه قالب نمیتواند خالی باشد",
|
||||||
|
ALIASES_EMAIL = "آدرس ایمیل الیاس باید معتبر باشد",
|
||||||
|
FORWARDERS_EMAIL_VALID = "آدرس ایمیل فوروارد باید معتبر باشد",
|
||||||
|
FORWARDERS_ARRAY = "آدرس های فوروارد باید یک آرایه باشد",
|
||||||
|
FORWARDERS_STRING = "آدرس های فوروارد باید یک رشته باشد",
|
||||||
|
PROFILE_PICTURE_URL = "آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد",
|
||||||
|
USER_UPDATED_SUCCESSFULLY = "کاربر با موفقیت به روز رسانی شد",
|
||||||
|
PROFILE_PICTURE_NOT_EMPTY = "تصویر پروفایل نمیتواند خالی باشد",
|
||||||
|
PUSH_TOKEN_NOT_FOUND = "توکن پوش نامعتبر است",
|
||||||
|
PUSH_TOKEN_ADDED_SUCCESSFULLY = "توکن پوش با موفقیت اضافه شد",
|
||||||
|
NO_PUSH_TOKENS_FOUND = "توکن پوشی برای این کاربر یافت نشد",
|
||||||
|
PUSH_TOKEN_REMOVED_SUCCESSFULLY = "توکن پوش با موفقیت حذف شد",
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = "وضعیت باید یکی از مقادیر ۰ و ۱ باشد",
|
||||||
|
DATE_MUST_BE_DATE = "تاریخ باید یک تاریخ معتبر به صورت رشته باشد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum UploaderMessage {
|
||||||
|
UPLOAD_FILE_INVALID = "فایل معتبر نیست",
|
||||||
|
UPLOAD_FILE_TOO_LARGE = "فایل بزرگتر از حد مجاز است . حداکثر حجم فایل [MAX_FILE_SIZE] مگابایت است",
|
||||||
|
UPLOAD_FILE_TYPE_NOT_SUPPORTED = "نوع فایل معتبر نیست . مجاز است : [MIME_TYPES]",
|
||||||
|
UPLOAD_FILE_SUCCESS = "فایل با موفقیت آپلود شد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum EmailMessage {
|
||||||
|
EMAIL_VERIFICATION = "تایید ایمیل",
|
||||||
|
EMAIL_SENDING_FAILED = "ارسال ایمیل با خطا مواجه شد",
|
||||||
|
LOGIN = "ورود به سیستم",
|
||||||
|
INVOICE = "فاکتور",
|
||||||
|
WALLET = "اعلان کیف پول",
|
||||||
|
TICKET = "تیکت پشتیبانی",
|
||||||
|
ANNOUNCEMENT = "اعلان",
|
||||||
|
PAYMENT_REMINDER = "یادآوری پرداخت",
|
||||||
|
PAYMENT_CANCELLATION = "لغو پرداخت",
|
||||||
|
EMAIL_SENDING_SUCCESS = "ارسال ایمیل با موفقیت انجام شد",
|
||||||
|
EMAIL_FORWARD_SUCCESS = "فوروارد ایمیل با موفقیت انجام شد",
|
||||||
|
EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = "وضعیت ایمیل با موفقیت دریافت شد",
|
||||||
|
MESSAGES_SEARCHED_SUCCESSFULLY = "جستجوی ایمیل با موفقیت انجام شد",
|
||||||
|
MESSAGES_LISTED_SUCCESSFULLY = "لیست ایمیل ها با موفقیت دریافت شد",
|
||||||
|
MESSAGE_RETRIEVED_SUCCESSFULLY = "ایمیل با موفقیت دریافت شد",
|
||||||
|
MESSAGE_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد",
|
||||||
|
USER_ID_REQUIRED = "شناسه کاربری مورد نیاز است",
|
||||||
|
USER_ID_MUST_BE_STRING = "شناسه کاربری باید یک رشته باشد",
|
||||||
|
MAILBOX_ID_REQUIRED = "شناسه میل باکس ایمیل مورد نیاز است",
|
||||||
|
MAILBOX_ID_MUST_BE_STRING = "شناسه میل باکس ایمیل باید یک رشته باشد",
|
||||||
|
QUEUE_ID_REQUIRED = "شناسه پیشنویس ایمیل مورد نیاز است",
|
||||||
|
QUEUE_ID_MUST_BE_STRING = "شناسه پیشنویس ایمیل باید یک رشته باشد",
|
||||||
|
MESSAGE_ID_MUST_BE_NUMBER = "شناسه ایمیل باید یک عدد باشد",
|
||||||
|
FROM_ADDRESS_REQUIRED = "آدرس از مورد نیاز است",
|
||||||
|
|
||||||
|
// Validation Messages for Email DTOs
|
||||||
|
FROM_ADDRESS_MUST_BE_EMAIL = "آدرس فرستنده باید یک ایمیل معتبر باشد",
|
||||||
|
|
||||||
|
// Query validation messages
|
||||||
|
SEARCH_QUERY_PARAMETER_MUST_BE_STRING = "پارامتر جستجو باید یک رشته باشد",
|
||||||
|
MESSAGE_IDS_MUST_BE_STRING = "شناسههای پیام باید یک رشته باشد",
|
||||||
|
THREAD_ID_MUST_BE_STRING = "شناسه رشته گفتگو باید یک رشته باشد",
|
||||||
|
OR_QUERY_MUST_BE_OBJECT = "پارامتر OR باید یک شیء باشد",
|
||||||
|
DATE_START_MUST_BE_DATE_STRING = "تاریخ شروع باید یک رشته تاریخ معتبر باشد",
|
||||||
|
DATE_END_MUST_BE_DATE_STRING = "تاریخ پایان باید یک رشته تاریخ معتبر باشد",
|
||||||
|
TO_ADDRESS_MUST_BE_STRING = "آدرس گیرنده باید یک رشته باشد",
|
||||||
|
MIN_SIZE_MUST_BE_NUMBER = "حداقل اندازه باید یک عدد باشد",
|
||||||
|
MIN_SIZE_MUST_BE_NON_NEGATIVE = "حداقل اندازه نمیتواند منفی باشد",
|
||||||
|
MAX_SIZE_MUST_BE_NUMBER = "حداکثر اندازه باید یک عدد باشد",
|
||||||
|
MAX_SIZE_MUST_BE_NON_NEGATIVE = "حداکثر اندازه نمیتواند منفی باشد",
|
||||||
|
ATTACHMENTS_FILTER_MUST_BE_BOOLEAN = "فیلتر پیوستها باید بولین باشد",
|
||||||
|
FLAGGED_FILTER_MUST_BE_BOOLEAN = "فیلتر پیامهای علامتدار باید بولین باشد",
|
||||||
|
UNSEEN_FILTER_MUST_BE_BOOLEAN = "فیلتر پیامهای خواندهنشده باید بولین باشد",
|
||||||
|
INCLUDE_HEADERS_MUST_BE_STRING = "لیست هدرها باید یک رشته باشد",
|
||||||
|
SEARCHABLE_FILTER_MUST_BE_BOOLEAN = "فیلتر قابل جستجو باید بولین باشد",
|
||||||
|
THREAD_COUNTERS_MUST_BE_BOOLEAN = "شمارنده رشته گفتگو باید بولین باشد",
|
||||||
|
ORDER_MUST_BE_VALID = "ترتیب باید یکی از مقادیر asc یا desc باشد",
|
||||||
|
NEXT_CURSOR_MUST_BE_STRING = "نشانگر صفحه بعد باید یک رشته باشد",
|
||||||
|
PREVIOUS_CURSOR_MUST_BE_STRING = "نشانگر صفحه قبل باید یک رشته باشد",
|
||||||
|
|
||||||
|
RECIPIENT_NAME_MUST_BE_STRING = "نام گیرنده باید یک رشته باشد",
|
||||||
|
RECIPIENT_ADDRESS_REQUIRED = "آدرس گیرنده مورد نیاز است",
|
||||||
|
RECIPIENT_ADDRESS_MUST_BE_EMAIL = "آدرس گیرنده باید یک ایمیل معتبر باشد",
|
||||||
|
HEADER_KEY_REQUIRED = "کلید هدر مورد نیاز است",
|
||||||
|
HEADER_KEY_MUST_BE_STRING = "کلید هدر باید یک رشته باشد",
|
||||||
|
HEADER_VALUE_REQUIRED = "مقدار هدر مورد نیاز است",
|
||||||
|
HEADER_VALUE_MUST_BE_STRING = "مقدار هدر باید یک رشته باشد",
|
||||||
|
ATTACHMENT_FILENAME_MUST_BE_STRING = "نام فایل پیوست باید یک رشته باشد",
|
||||||
|
ATTACHMENT_CONTENT_TYPE_MUST_BE_STRING = "نوع محتوای پیوست باید یک رشته باشد",
|
||||||
|
ATTACHMENT_ENCODING_MUST_BE_STRING = "کدگذاری پیوست باید یک رشته باشد",
|
||||||
|
ATTACHMENT_CONTENT_TRANSFER_ENCODING_MUST_BE_STRING = "کدگذاری انتقال محتوای پیوست باید یک رشته باشد",
|
||||||
|
ATTACHMENT_CONTENT_DISPOSITION_INVALID = "نحوه نمایش پیوست باید inline یا attachment باشد",
|
||||||
|
ATTACHMENT_CONTENT_REQUIRED = "محتوای پیوست مورد نیاز است",
|
||||||
|
ATTACHMENT_CONTENT_MUST_BE_STRING = "محتوای پیوست باید یک رشته باشد",
|
||||||
|
ATTACHMENT_CID_MUST_BE_STRING = "شناسه محتوای پیوست باید یک رشته باشد",
|
||||||
|
DRAFT_MAILBOX_REQUIRED = "شناسه صندوق پیشنویس مورد نیاز است",
|
||||||
|
DRAFT_MAILBOX_MUST_BE_STRING = "شناسه صندوق پیشنویس باید یک رشته باشد",
|
||||||
|
DRAFT_ID_REQUIRED = "شناسه پیشنویس مورد نیاز است",
|
||||||
|
DRAFT_ID_MUST_BE_NUMBER = "شناسه پیشنویس باید یک عدد باشد",
|
||||||
|
ENVELOPE_FROM_REQUIRED = "آدرس فرستنده در envelope مورد نیاز است",
|
||||||
|
ENVELOPE_TO_REQUIRED = "آدرس گیرنده در envelope مورد نیاز است",
|
||||||
|
ENVELOPE_TO_MUST_BE_ARRAY = "آدرس گیرنده در envelope باید یک آرایه باشد",
|
||||||
|
MAILBOX_MUST_BE_STRING = "شناسه صندوق باید یک رشته باشد",
|
||||||
|
REPLY_TO_MUST_BE_VALID = "آدرس پاسخ باید معتبر باشد",
|
||||||
|
TO_ADDRESSES_REQUIRED = "آدرسهای گیرنده مورد نیاز است",
|
||||||
|
TO_ADDRESSES_MUST_BE_ARRAY = "آدرسهای گیرنده باید یک آرایه باشد",
|
||||||
|
CC_ADDRESSES_MUST_BE_ARRAY = "آدرسهای کپی کربن باید یک آرایه باشد",
|
||||||
|
BCC_ADDRESSES_MUST_BE_ARRAY = "آدرسهای کپی مخفی باید یک آرایه باشد",
|
||||||
|
HEADERS_MUST_BE_ARRAY = "هدرهای سفارشی باید یک آرایه باشد",
|
||||||
|
SUBJECT_MUST_BE_STRING = "موضوع ایمیل باید یک رشته باشد",
|
||||||
|
TEXT_CONTENT_MUST_BE_STRING = "محتوای متنی باید یک رشته باشد",
|
||||||
|
HTML_CONTENT_MUST_BE_STRING = "محتوای HTML باید یک رشته باشد",
|
||||||
|
ATTACHMENTS_MUST_BE_ARRAY = "پیوستها باید یک آرایه باشد",
|
||||||
|
SESSION_ID_MUST_BE_STRING = "شناسه جلسه باید یک رشته باشد",
|
||||||
|
IP_ADDRESS_MUST_BE_STRING = "آدرس IP باید یک رشته باشد",
|
||||||
|
IS_DRAFT_MUST_BE_BOOLEAN = "وضعیت پیشنویس باید بولین باشد",
|
||||||
|
SEND_TIME_MUST_BE_DATE = "زمان ارسال باید یک تاریخ معتبر باشد",
|
||||||
|
UPLOAD_ONLY_MUST_BE_BOOLEAN = "حالت فقط آپلود باید بولین باشد",
|
||||||
|
ENVELOPE_MUST_BE_VALID = "envelope باید معتبر باشد",
|
||||||
|
|
||||||
|
// Message operations
|
||||||
|
MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY = "پیام با موفقیت به عنوان خوانده شده علامت گذاری شد",
|
||||||
|
ALL_MESSAGES_MARKED_AS_READ_SUCCESSFULLY = "تمام پیام ها با موفقیت به عنوان خوانده شده علامت گذاری شدند",
|
||||||
|
MESSAGE_MARK_AS_SEEN_FAILED = "علامت گذاری پیام با خطا مواجه شد",
|
||||||
|
SENT_MESSAGES_RETRIEVED_SUCCESSFULLY = "پیام های ارسالی با موفقیت دریافت شد",
|
||||||
|
SENT_MESSAGES_RETRIEVAL_FAILED = "دریافت پیام های ارسالی با خطا مواجه شد",
|
||||||
|
TRASH_MESSAGES_RETRIEVED_SUCCESSFULLY = "پیام های حذف شده با موفقیت دریافت شد",
|
||||||
|
TRASH_MESSAGES_RETRIEVAL_FAILED = "دریافت پیام های حذف شده با خطا مواجه شد",
|
||||||
|
DRAFT_UPDATED_SUCCESSFULLY = "پیش نویس با موفقیت به روز رسانی شد",
|
||||||
|
DRAFT_SENT_SUCCESSFULLY = "پیش نویس با موفقیت ارسال شد",
|
||||||
|
DRAFT_DELETED_SUCCESSFULLY = "پیش نویس با موفقیت حذف شد",
|
||||||
|
MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت به آرشیو منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به علاقه مندی ها منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = "پیام با موفقیت به سطل زباله منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY = "پیام با موفقیت به جانک منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = "پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY = "پیام با موفقیت از جانک به صندوق ورودی منتقل شد",
|
||||||
|
MESSAGE_NOT_FOUND = "پیام یافت نشد",
|
||||||
|
|
||||||
|
// Bulk action messages
|
||||||
|
BULK_ACTION_COMPLETED_SUCCESSFULLY = "[count] پیام با موفقیت [action] شد",
|
||||||
|
BULK_ACTION_PARTIALLY_COMPLETED = "[successful] از [total] پیام [action] شد. [failed] پیام با خطا مواجه شد",
|
||||||
|
BULK_ACTION_FAILED = "هیچ پیامی [action] نشد. تمام [total] تلاش ناموفق بود",
|
||||||
|
|
||||||
|
// Bulk action display names
|
||||||
|
BULK_ACTION_SEEN = "خوانده شده علامتگذاری",
|
||||||
|
BULK_ACTION_DELETE = "حذف",
|
||||||
|
BULK_ACTION_ARCHIVE = "آرشیو",
|
||||||
|
BULK_ACTION_UNARCHIVE = "از آرشیو خارج",
|
||||||
|
BULK_ACTION_FAVORITE = "به علاقه مندی ها منتقل",
|
||||||
|
BULK_ACTION_UNFAVORITE = "از علاقه مندی ها حذف",
|
||||||
|
BULK_ACTION_JUNK = "به جانک منتقل",
|
||||||
|
BULK_ACTION_NOTJUNK = "از جانک از حذف",
|
||||||
|
BULK_ACTION_TRASH = "به سطل زباله منتقل",
|
||||||
|
BULK_ACTION_RESTORE = "از سطل زباله بازیابی",
|
||||||
|
BULK_ACTION_PROCESSED = "پردازش",
|
||||||
|
USER_NOT_FOUND = "کاربر یافت نشد",
|
||||||
|
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "اولویت باید یکی از مقادیر high, normal, low باشد",
|
||||||
|
SUBJECT_REQUIRED = "موضوع ایمیل مورد نیاز است",
|
||||||
|
ALL_MESSAGES_DELETED_SUCCESSFULLY = "همه پیام ها با موفقیت حذف شدند",
|
||||||
|
MAILBOX_ID_MUST_BE_MONGO_ID = "شناسه میل باکس ایمیل باید یک آی دی معتبر باشد",
|
||||||
|
MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY = "پیام با موفقیت به عنوان خوانده نشده علامت گذاری شد",
|
||||||
|
MESSAGE_MARK_AS_UNSEEN_FAILED = "علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد",
|
||||||
|
ATTACHMENT_ID_MUST_BE_STRING = "شناسه پیوست باید یک رشته باشد",
|
||||||
|
IS_FORWARD_MUST_BE_BOOLEAN = "IS_FORWARD_MUST_BE_BOOLEAN",
|
||||||
|
|
||||||
|
// Favorite/Star messages (Gmail-like behavior)
|
||||||
|
MESSAGE_MARKED_AS_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به عنوان مورد علاقه علامت گذاری شد",
|
||||||
|
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از مورد علاقه حذف شد",
|
||||||
|
TRASH_EMPTYED_SUCCESSFULLY = "سطل زباله با موفقیت خالی شد",
|
||||||
|
SPAM_EMPTYED_SUCCESSFULLY = "همه پیام های اسپم با موفقیت حذف شدند",
|
||||||
|
SAVE_DRAFT_SUCCESS = "پیش نویس با موفقیت ذخیره شد",
|
||||||
|
HTML_CONTENT_REQUIRED = "محتوای HTML مورد نیاز است",
|
||||||
|
TEXT_CONTENT_REQUIRED = "TEXT_CONTENT_REQUIRED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum WebSocketMessage {
|
||||||
|
// Connection Messages
|
||||||
|
WS_CONNECTION_ESTABLISHED = "اتصال وب سوکت با موفقیت برقرار شد",
|
||||||
|
WS_CONNECTION_FAILED = "اتصال وب سوکت ناموفق بود",
|
||||||
|
WS_AUTHENTICATION_REQUIRED = "احراز هویت وب سوکت مورد نیاز است",
|
||||||
|
WS_AUTHENTICATION_FAILED = "احراز هویت وب سوکت ناموفق بود",
|
||||||
|
WS_AUTHENTICATION_SUCCESS = "احراز هویت وب سوکت با موفقیت انجام شد",
|
||||||
|
WS_INVALID_TOKEN = "توکن وب سوکت نامعتبر است",
|
||||||
|
WS_TOKEN_EXPIRED = "توکن وب سوکت منقضی شده است",
|
||||||
|
|
||||||
|
// Room Management Messages
|
||||||
|
JOINED_ROOM_SUCCESS = "با موفقیت به اتاق متصل شدید",
|
||||||
|
LEFT_ROOM_SUCCESS = "با موفقیت از اتاق خارج شدید",
|
||||||
|
ROOM_NOT_FOUND = "اتاق یافت نشد",
|
||||||
|
UNAUTHORIZED_ROOM_ACCESS = "دسترسی غیرمجاز به اتاق",
|
||||||
|
|
||||||
|
// Session Management Messages
|
||||||
|
WS_SESSION_CREATED = "جلسه وب سوکت ایجاد شد",
|
||||||
|
WS_SESSION_JOINED = "به جلسه وب سوکت پیوستید",
|
||||||
|
WS_SESSION_LEFT = "از جلسه وب سوکت خارج شدید",
|
||||||
|
WS_SESSION_EXPIRED = "جلسه وب سوکت منقضی شده است",
|
||||||
|
WS_SESSION_NOT_FOUND = "جلسه وب سوکت یافت نشد",
|
||||||
|
|
||||||
|
// Error Messages
|
||||||
|
WS_CONNECTION_ERROR = "خطا در اتصال وب سوکت",
|
||||||
|
INVALID_EVENT = "رویداد نامعتبر",
|
||||||
|
INVALID_DATA = "داده نامعتبر",
|
||||||
|
WS_RATE_LIMIT_EXCEEDED = "تعداد درخواستهای وب سوکت از حد مجاز بیشتر است",
|
||||||
|
INTERNAL_ERROR = "خطای داخلی سرور",
|
||||||
|
|
||||||
|
// Notification Messages
|
||||||
|
NEW_MESSAGE_NOTIFICATION = "پیام جدید دریافت شد",
|
||||||
|
MESSAGE_STATUS_UPDATED = "وضعیت پیام بهروزرسانی شد",
|
||||||
|
MESSAGE_MOVED_NOTIFICATION = "پیام جابجا شد",
|
||||||
|
MESSAGE_DELETED_NOTIFICATION = "پیام حذف شد",
|
||||||
|
|
||||||
|
// General Messages
|
||||||
|
OPERATION_SUCCESS = "عملیات با موفقیت انجام شد",
|
||||||
|
OPERATION_FAILED = "عملیات ناموفق بود",
|
||||||
|
PERMISSION_DENIED = "مجوز دسترسی ندارید",
|
||||||
|
RESOURCE_NOT_FOUND = "منبع یافت نشد",
|
||||||
|
|
||||||
|
// Authentication errors
|
||||||
|
AUTHENTICATION_REQUIRED = "احراز هویت ضروری است",
|
||||||
|
INVALID_TOKEN = "توکن احراز هویت نامعتبر است",
|
||||||
|
TOKEN_EXPIRED = "توکن منقضی شده است",
|
||||||
|
UNAUTHORIZED_ACCESS = "دسترسی غیرمجاز",
|
||||||
|
USER_NOT_AUTHENTICATED = "کاربر احراز هویت نشده است",
|
||||||
|
|
||||||
|
// Connection errors
|
||||||
|
CONNECTION_FAILED = "اتصال برقرار نشد",
|
||||||
|
WEBSOCKET_ERROR = "خطا در ارتباط WebSocket",
|
||||||
|
CONNECTION_TIMEOUT = "زمان اتصال به پایان رسید",
|
||||||
|
CONNECTION_REFUSED = "اتصال رد شد",
|
||||||
|
|
||||||
|
// Session errors
|
||||||
|
SESSION_NOT_FOUND = "جلسه چت یافت نشد",
|
||||||
|
SESSION_CREATION_FAILED = "ایجاد جلسه چت با شکست مواجه شد",
|
||||||
|
SESSION_ACCESS_DENIED = "دسترسی به جلسه چت مجاز نیست",
|
||||||
|
SESSION_EXPIRED = "جلسه چت منقضی شده است",
|
||||||
|
SESSION_ALREADY_EXISTS = "جلسه چت قبلاً وجود دارد",
|
||||||
|
|
||||||
|
// Message errors
|
||||||
|
MESSAGE_TOO_LONG = "پیام از حداکثر طول مجاز تجاوز کرده است",
|
||||||
|
MESSAGE_EMPTY = "پیام نمیتواند خالی باشد",
|
||||||
|
MESSAGE_SEND_FAILED = "ارسال پیام با شکست مواجه شد",
|
||||||
|
INVALID_MESSAGE_FORMAT = "فرمت پیام نامعتبر است",
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
RATE_LIMIT_EXCEEDED = "تعداد پیامهای ارسالی بیش از حد مجاز است",
|
||||||
|
TOO_MANY_CONNECTIONS = "تعداد اتصالات بیش از حد مجاز است",
|
||||||
|
|
||||||
|
// Service errors
|
||||||
|
LLM_SERVICE_ERROR = "سرویس هوش مصنوعی موقتاً در دسترس نیست",
|
||||||
|
SERVICE_UNAVAILABLE = "سرویس موقتاً در دسترس نیست",
|
||||||
|
INTERNAL_SERVER_ERROR = ".خطای داخلی سرور",
|
||||||
|
|
||||||
|
// Validation errors
|
||||||
|
INVALID_SESSION_ID = "شناسه جلسه نامعتبر است",
|
||||||
|
INVALID_USER_ID = "شناسه کاربر نامعتبر است",
|
||||||
|
INVALID_MESSAGE_ID = "شناسه پیام نامعتبر است",
|
||||||
|
REQUIRED_FIELD_MISSING = "فیلد ضروری موجود نیست",
|
||||||
|
|
||||||
|
// Success messages
|
||||||
|
AUTHENTICATED = "احراز هویت موفقیتآمیز",
|
||||||
|
SESSION_CREATED = "جلسه چت با موفقیت ایجاد شد",
|
||||||
|
CHAT_JOINED = "با موفقیت به چت متصل شدید",
|
||||||
|
MESSAGE_SENT = "پیام ارسال شد",
|
||||||
|
CONNECTION_ESTABLISHED = "اتصال برقرار شد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum SmsMessage {
|
||||||
|
SEND_SMS_ERROR = "خطا در ارسال پیامک",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum BusinessMessage {
|
||||||
|
NOT_FOUND = "کسب و کار یافت نشد",
|
||||||
|
BUSINESS_ID_REQUIRED = "شناسه کسب و کار مورد نیاز است",
|
||||||
|
SLUG_REQUIRED = "اسلاگ مورد نیاز است",
|
||||||
|
SLUG_MUST_BE_STRING = "اسلاگ باید یک رشته باشد",
|
||||||
|
DOMAIN_INVALID = "دامنه وارد شده معتبر نیست",
|
||||||
|
DOMAIN_UPDATED = "دامنه با موفقیت بهروزرسانی شد",
|
||||||
|
DOMAIN_VERIFICATION_INITIATED = "فرآیند تایید دامنه آغاز شد",
|
||||||
|
DOMAIN_VERIFICATION_FAILED = "تایید دامنه با خطا مواجه شد",
|
||||||
|
DOMAIN_VERIFICATION_SUCCESS = "دامنه با موفقیت تایید شد",
|
||||||
|
DOMAIN_VERIFICATION_METHOD_INVALID = "روش تایید دامنه نامعتبر است",
|
||||||
|
DOMAIN_ALREADY_VERIFIED = "این دامنه قبلا تایید شده است",
|
||||||
|
DOMAIN_REQUIRED = "دامنه مورد نیاز است",
|
||||||
|
DOMAIN_MUST_BE_STRING = "دامنه باید یک رشته باشد",
|
||||||
|
STAFF_NOT_FOUND = "کاربر ادمین یافت نشد",
|
||||||
|
ZARINPAL_MERCHANT_ID_REQUIRED = "شناسه مرچنت زرین پال مورد نیاز است",
|
||||||
|
ZARINPAL_MERCHANT_ID_STRING = "شناسه مرچنت زرین پال باید یک رشته باشد",
|
||||||
|
LOGO_URL_REQUIRED = "آدرس تصویر لوگو مورد نیاز است",
|
||||||
|
LOGO_URL_STRING = "آدرس تصویر لوگو باید یک رشته باشد",
|
||||||
|
LOGO_URL_INVALID = "آدرس تصویر لوگو معتبر نیست",
|
||||||
|
BUSINESS_SETTINGS_UPDATED = "تنظیمات کسب و کار با موفقیت به روز رسانی شد",
|
||||||
|
NAME_REQUIRED = "نام کسب و کار مورد نیاز است",
|
||||||
|
NAME_STRING = "نام کسب و کار باید یک رشته باشد",
|
||||||
|
DOMAIN_CONNECTED = "دامنه با موفقیت متصل شد",
|
||||||
|
DOMAIN_NOT_CONNECTED = "دامنه به سرور ما متصل نشد",
|
||||||
|
DOMAIN_NOT_FOUND = "دامنه یافت نشد",
|
||||||
|
DOMAIN_VERIFICATION_TOKEN_REQUIRED = "توکن تایید دامنه مورد نیاز است",
|
||||||
|
INSUFFICIENT_QUOTA = "حجم شما برای ساخت ایمیل کافی نمی باشد، لطفا حجم را افزایش دهید",
|
||||||
|
QUOTA_DOWNGRADE_NOT_ALLOWED = "کاهش حجم ایمیل مجاز نیست، فقط میتوانید حجم را افزایش دهید",
|
||||||
|
QUOTA_AMOUNT_REQUIRED = "مقدار حجم مورد نیاز است",
|
||||||
|
QUOTA_AMOUNT_MUST_BE_NUMBER = "مقدار حجم باید یک عدد باشد",
|
||||||
|
QUOTA_AMOUNT_MUST_BE_POSITIVE = "مقدار حجم باید مثبت باشد",
|
||||||
|
CALLBACK_URL_MUST_BE_STRING = "آدرس بازگشت باید یک رشته باشد",
|
||||||
|
CALLBACK_URL_INVALID = "آدرس بازگشت معتبر نیست",
|
||||||
|
PURCHASE_DESCRIPTION_MUST_BE_STRING = "توضیحات خرید باید یک رشته باشد",
|
||||||
|
QUOTA_PURCHASE_SUCCESS = "درخواست خرید حجم با موفقیت ثبت شد",
|
||||||
|
QUOTA_PURCHASE_FAILED = "خرید حجم با خطا مواجه شد",
|
||||||
|
PAYMENT_GATEWAY_ERROR = "خطا در اتصال به درگاه پرداخت",
|
||||||
|
QUOTA_UPDATED = "حجم ایمیل با موفقیت به روز شد",
|
||||||
|
USER_DOES_NOT_HAVE_ACCESS_TO_SERVICE = "کاربر دسترسی به این سرویس را ندارد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum DomainMessage {
|
||||||
|
// Domain Ownership & Verification
|
||||||
|
NOT_BELONG_TO_BUSINESS = "دامنه به این کسب و کار تعلق ندارد",
|
||||||
|
NOT_VERIFIED = "دامنه باید تایید شده باشد",
|
||||||
|
DOMAIN_NOT_FOUND = "دامنه یافت نشد",
|
||||||
|
DOMAIN_ALREADY_EXISTS = "دامنه [name] قبلا ثبت شده است",
|
||||||
|
DOMAIN_DELETED_SUCCESSFULLY = "دامنه با موفقیت حذف شد",
|
||||||
|
VERIFICATION_NOT_FOUND = "تایید دامنه یافت نشد",
|
||||||
|
NO_DOMAIN_FOUND = "دامنه ای یافت نشد",
|
||||||
|
DOMAIN_CHECKED = "دامنه با موفقیت بررسی شد",
|
||||||
|
|
||||||
|
// Domain Creation & Setup
|
||||||
|
DOMAIN_CREATED_WITH_RECORDS = "دامنه [name] با موفقیت ایجاد شد و تمام رکوردهای مورد نیاز تولید گردید",
|
||||||
|
ALL_REQUIRED_RECORDS_GENERATED = "تمام رکوردهای مورد نیاز (MX, SPF, DMARC, DKIM) به طور خودکار برای شما تولید شدهاند",
|
||||||
|
ESTIMATED_PROPAGATION_TIME = "تغییرات DNS معمولاً ۱۵ دقیقه تا ۴۸ ساعت در سراسر جهان منتشر میشود",
|
||||||
|
|
||||||
|
// DNS Records Instructions
|
||||||
|
ADD_MX_RECORD = "یک رکورد MX با نام [name] که به [value] با اولویت [priority] اشاره کند اضافه کنید",
|
||||||
|
ADD_TXT_RECORD = "یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید",
|
||||||
|
ADD_DKIM_RECORD = "یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (کلید عمومی DKIM)",
|
||||||
|
ADD_DMARC_RECORD = "یک رکورد TXT با نام [name] و مقدار [value] اضافه کنید (سیاست DMARC)",
|
||||||
|
ADD_CNAME_RECORD = "یک رکورد CNAME با نام [name] که به [value] اشاره کند اضافه کنید",
|
||||||
|
ADD_GENERIC_RECORD = "یک رکورد [type] با نام [name] و مقدار [value] اضافه کنید",
|
||||||
|
|
||||||
|
// Domain Setup Steps
|
||||||
|
STEP_LOGIN_DNS_PANEL = "وارد پنل مدیریت DNS ثبت کننده دامنه خود شوید",
|
||||||
|
STEP_ADD_ALL_RECORDS = "تمام رکوردهای DNS مورد نیاز لیست شده در بالا را به زون DNS دامنه خود اضافه کنید",
|
||||||
|
STEP_INCLUDE_ALL_TYPES = "مطمئن شوید که رکوردهای MX، SPF، DMARC و DKIM را برای عملکرد کامل ایمیل شامل کردهاید",
|
||||||
|
STEP_WAIT_PROPAGATION = "منتظر انتشار DNS بمانید (معمولاً ۱۵ دقیقه تا ۴۸ ساعت)",
|
||||||
|
STEP_USE_CHECK_ENDPOINT = "از نقطه پایانی 'بررسی DNS' برای تایید تنظیم صحیح رکوردهای خود استفاده کنید",
|
||||||
|
STEP_DOMAIN_READY = "پس از تایید، دامنه شما برای سرویسهای ایمیل آماده خواهد بود!",
|
||||||
|
|
||||||
|
// Status Messages
|
||||||
|
RECORDS_PENDING_VERIFICATION = "[count] رکورد DNS هنوز در انتظار تایید هستند",
|
||||||
|
ALL_REQUIRED_RECORDS_MUST_BE_CONFIGURED = "تمام [count] رکورد مورد نیاز باید برای کار درست ایمیل تنظیم شوند",
|
||||||
|
DOMAIN_FULLY_VERIFIED = "دامنه شما به طور کامل تایید شده و برای ایمیل آماده است!",
|
||||||
|
EMAIL_ACCOUNTS_READY = "اکنون میتوانید حسابهای ایمیل ایجاد کرده و شروع به ارسال/دریافت ایمیل کنید",
|
||||||
|
|
||||||
|
// Verification Status
|
||||||
|
DOMAIN_VERIFICATION_INITIATED = "فرآیند تایید دامنه آغاز شد",
|
||||||
|
DOMAIN_VERIFICATION_FAILED = "تایید دامنه با خطا مواجه شد",
|
||||||
|
DOMAIN_VERIFICATION_SUCCESS = "دامنه با موفقیت تایید شد",
|
||||||
|
DOMAIN_VERIFICATION_PENDING = "تایید دامنه در انتظار است",
|
||||||
|
|
||||||
|
// DNS Record Status
|
||||||
|
DNS_RECORD_VERIFIED = "رکورد DNS تایید شد",
|
||||||
|
DNS_RECORD_FAILED = "رکورد DNS تایید نشد",
|
||||||
|
DNS_RECORD_NOT_FOUND_OR_MISMATCH = "رکورد DNS یافت نشد یا مقدار مطابقت ندارد",
|
||||||
|
|
||||||
|
// DKIM Messages
|
||||||
|
DKIM_ENABLED_AUTOMATICALLY = "DKIM به طور خودکار برای دامنه [name] فعال شد",
|
||||||
|
DKIM_FAILED_TO_CREATE = "ایجاد DKIM برای دامنه [name] ناموفق بود",
|
||||||
|
DKIM_ENABLED_SUCCESSFULLY = "DKIM برای دامنه [name] با موفقیت فعال شد",
|
||||||
|
|
||||||
|
// Error Messages
|
||||||
|
FAILED_TO_VERIFY_DOMAIN = "تایید دامنه [name] ناموفق بود",
|
||||||
|
FAILED_TO_SETUP_MAIL_SERVER = "راهاندازی دامنه [name] در سرور ایمیل ناموفق بود",
|
||||||
|
DOMAIN_CREATION_FAILED = "ایجاد دامنه ناموفق بود",
|
||||||
|
DNS_VERIFICATION_ATTEMPTS_EXCEEDED = "تعداد تلاشهای تایید DNS از حد مجاز بیشتر شد",
|
||||||
|
|
||||||
|
// Service Messages
|
||||||
|
DOMAIN_UPDATED_SUCCESSFULLY = "دامنه بروزرسانی شد",
|
||||||
|
VERIFICATION_TOKEN_PREFIX = "توکن تایید: [token]",
|
||||||
|
RECORD_NEEDS_FIXING = "رکورد [type] برای [name] نیاز به اصلاح دارد: [error]",
|
||||||
|
RECORD_PENDING_VERIFICATION_DETAIL = "رکورد [type] برای [name] هنوز در انتظار تایید است. لطفا مطمئن شوید که انتشار DNS کامل شده است.",
|
||||||
|
RECORD_DESCRIPTION_EMAIL_FUNCTIONALITY = "رکورد [type] برای عملکرد ایمیل",
|
||||||
|
BUSINESS_ALREADY_HAS_DOMAIN = "این کسب و کار قبلا دامنه ای دارد",
|
||||||
|
CAN_NOT_USE_THIS_DOMAIN = "این دامنه نمیتواند ثبت شود",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum MailServerMessage {
|
||||||
|
FAILED_TO_CREATE_ACCOUNT = "خطا در ایجاد ایمیل",
|
||||||
|
FAILED_TO_CREATE_DKIM_KEY = "خطا در ایجاد کلید DKIM",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum DnsRecordMessage {
|
||||||
|
DNS_RECORD_NOT_FOUND = "رکورد DNS یافت نشد",
|
||||||
|
DNS_RECORD_DELETED = "رکورد DNS با موفقیت حذف شد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum RoleMessage {
|
||||||
|
NOT_FOUND = "نقش یافت نشد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum SignatureMessage {
|
||||||
|
NAME_NOT_EMPTY = "نام امضاء نمیتواند خالی باشد",
|
||||||
|
NAME_MAX_LENGTH = "نام امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد",
|
||||||
|
NAME_STRING = "نام امضاء باید یک رشته باشد",
|
||||||
|
TEXT_CONTENT_NOT_EMPTY = "محتوای متنی امضاء نمیتواند خالی باشد",
|
||||||
|
TEXT_CONTENT_STRING = "محتوای متنی امضاء باید یک رشته باشد",
|
||||||
|
TEXT_CONTENT_MAX_LENGTH = "محتوای متنی امضاء نمیتواند بیشتر از حداکثر طول مجاز باشد",
|
||||||
|
IS_ACTIVE_NOT_EMPTY = "وضعیت فعال بودن نمیتواند خالی باشد",
|
||||||
|
IS_ACTIVE_BOOLEAN = "وضعیت فعال بودن باید بولین باشد",
|
||||||
|
IS_DEFAULT_NOT_EMPTY = "وضعیت پیش فرض نمیتواند خالی باشد",
|
||||||
|
IS_DEFAULT_BOOLEAN = "وضعیت پیش فرض باید بولین باشد",
|
||||||
|
CREATED = "امضاء با موفقیت ایجاد شد",
|
||||||
|
NOT_FOUND = "امضاء یافت نشد",
|
||||||
|
DELETED = "امضاء با موفقیت حذف شد",
|
||||||
|
TOGGLE_ACTIVE = "وضعیت امضاء با موفقیت تغییر کرد",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TemplateMessage {
|
||||||
|
TEMPLATE_NOT_FOUND = "قالب یافت نشد",
|
||||||
|
TEMPLATE_CREATED_SUCCESSFULLY = "قالب با موفقیت ایجاد شد",
|
||||||
|
TEMPLATE_UPDATED_SUCCESSFULLY = "قالب با موفقیت به روزرسانی شد",
|
||||||
|
TEMPLATE_DELETED_SUCCESSFULLY = "قالب با موفقیت حذف شد",
|
||||||
|
TEMPLATE_RETRIEVED_SUCCESSFULLY = "قالب با موفقیت دریافت شد",
|
||||||
|
TEMPLATES_LISTED_SUCCESSFULLY = "لیست قالب ها با موفقیت دریافت شد",
|
||||||
|
TEMPLATE_NAME_REQUIRED = "نام قالب الزامی است",
|
||||||
|
TEMPLATE_NAME_STRING = "نام قالب باید یک رشته باشد",
|
||||||
|
TEMPLATE_NAME_MAX_LENGTH = "نام قالب نمیتواند بیشتر از ۲۵۵ کاراکتر باشد",
|
||||||
|
TEMPLATE_STRUCTURE_REQUIRED = "ساختار قالب الزامی است",
|
||||||
|
TEMPLATE_STRUCTURE_VALID = "ساختار قالب باید معتبر باشد",
|
||||||
|
TEMPLATE_RAW_HTML_STRING = "HTML خام باید یک رشته باشد",
|
||||||
|
TEMPLATE_BUSINESS_ID_REQUIRED = "شناسه کسب و کار الزامی است",
|
||||||
|
TEMPLATE_BUSINESS_ID_UUID = "شناسه کسب و کار باید یک UUID معتبر باشد",
|
||||||
|
TEMPLATE_ID_REQUIRED = "شناسه قالب الزامی است",
|
||||||
|
TEMPLATE_ID_UUID = "شناسه قالب باید یک UUID معتبر باشد",
|
||||||
|
TEMPLATE_ACCESS_DENIED = "شما دسترسی به این قالب را ندارید",
|
||||||
|
TEMPLATE_BUSINESS_NOT_FOUND = "کسب و کار مالک قالب یافت نشد",
|
||||||
|
SEARCH_STRING = "نام برای جستجو باید یک رشته باشد",
|
||||||
|
TEMPLATE_SELECTED_SUCCESSFULLY = "وضعیت قالب با موفقیت تغییر کرد",
|
||||||
|
TEMPLATE_THUMBNAIL_URL_VALID = "آدرس تصویر قالب باید یک آدرس معتبر باشد",
|
||||||
|
TEMPLATE_RAW_HTML_REQUIRED = "HTML خام قالب الزامی است",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum NotificationMessage {
|
||||||
|
TITLE_IS_REQUIRED = "عنوان اعلان الزامی است",
|
||||||
|
TITLE_STRING = "عنوان اعلان باید یک رشته باشد",
|
||||||
|
MESSAGE_IS_REQUIRED = "متن اعلان الزامی است",
|
||||||
|
MESSAGE_STRING = "متن اعلان باید یک رشته باشد",
|
||||||
|
USERID_IS_REQUIRED = "شناسه کاربر الزامی است",
|
||||||
|
USERID_IS_STRING = "شناسه کاربر باید یک رشته باشد",
|
||||||
|
USERID_IS_UUID = "شناسه کاربر باید یک UUID معتبر باشد",
|
||||||
|
TYPE_IS_REQUIRED = "نوع اعلان الزامی است",
|
||||||
|
TYPE_ENUM = "نوع اعلان باید یک enum معتبر باشد",
|
||||||
|
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی آن را ندارید",
|
||||||
|
LOGIN_MESSAGE = "ورود با موفقیت انجام شد در تاریخ [loginDate]",
|
||||||
|
LOGIN = "ورود",
|
||||||
|
NEW_EMAIL = "ایمیل جدید",
|
||||||
|
NEW_EMAIL_MESSAGE = "یک ایمیل جدید با موضوع [subject] دریافت شد",
|
||||||
|
CHANGE_PASSWORD_MESSAGE = "رمز عبور شما با موفقیت تغییر کرد",
|
||||||
|
CHANGE_PASSWORD = "تغییر رمز عبور",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum SettingMessageEnum {
|
||||||
|
SMS_MUST_BE_BOOLEAN = "تنظیمات ارسال پیامک باید یک boolean باشد",
|
||||||
|
EMAIL_MUST_BE_BOOLEAN = "تنظیمات ارسال ایمیل باید یک boolean باشد",
|
||||||
|
NOTIFICATION_NOT_FOUND_OR_ACCESS_DENIED = "اعلان یافت نشد یا دسترسی آن را ندارید",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum AiMessage {
|
||||||
|
NO_RESPONSE_FROM_AI_MODEL = "خطا در دریافت پاسخ از مدل AI",
|
||||||
|
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید",
|
||||||
|
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست",
|
||||||
|
DESCRIPTION_IS_REQUIRED = "شرح و هدف قالب الزامی است",
|
||||||
|
DESCRIPTION_MUST_BE_A_STRING = "شرح قالب باید یک رشته باشد",
|
||||||
|
DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS = "شرح قالب باید حداقل ۱۰ کاراکتر باشد",
|
||||||
|
DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS = "شرح قالب نمیتواند بیشتر از ۵۰۰ کاراکتر باشد",
|
||||||
|
THEME_MUST_BE_A_VALID_VALUE = "تم قالب باید یکی از مقادیر مجاز باشد",
|
||||||
|
STYLE_MUST_BE_A_VALID_VALUE = "سبک قالب باید یکی از مقادیر مجاز باشد",
|
||||||
|
PURPOSE_MUST_BE_A_VALID_VALUE = "هدف قالب باید یکی از مقادیر مجاز باشد",
|
||||||
|
TEMPLATE_NAME_MUST_BE_A_STRING = "نام قالب باید یک رشته باشد",
|
||||||
|
TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS = "نام قالب نمیتواند بیشتر از ۱۰۰ کاراکتر باشد",
|
||||||
|
PREFERRED_COLORS_MUST_BE_A_STRING = "رنگهای ترجیحی باید یک رشته باشد",
|
||||||
|
PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS = "رنگهای ترجیحی نمیتواند بیشتر از ۲۰۰ کاراکتر باشد",
|
||||||
|
PREFERRED_COLORS_IS_REQUIRED = "رنگهای ترجیحی الزامی است",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TwoFactorMessage {
|
||||||
|
TWO_FACTOR_SETUP_INITIATED = "راهاندازی احراز هویت دو مرحلهای با موفقیت آغاز شد",
|
||||||
|
TWO_FACTOR_ENABLED_SUCCESSFULLY = "احراز هویت دو مرحلهای با موفقیت فعال شد",
|
||||||
|
TWO_FACTOR_DISABLED_SUCCESSFULLY = "احراز هویت دو مرحلهای با موفقیت غیرفعال شد",
|
||||||
|
TWO_FACTOR_SETUP_FAILED = "راهاندازی احراز هویت دو مرحلهای ناموفق بود",
|
||||||
|
TWO_FACTOR_ENABLE_FAILED = "فعالسازی احراز هویت دو مرحلهای ناموفق بود",
|
||||||
|
TWO_FACTOR_TOKEN_VERIFIED_SUCCESSFULLY = "کد احراز هویت دو مرحلهای با موفقیت تأیید شد",
|
||||||
|
TWO_FACTOR_TOKEN_VERIFIED_FAILED = "کد احراز هویت دو مرحلهای نامعتبر میباشد",
|
||||||
|
TWO_FACTOR_NOT_ENABLED = "احراز هویت دو مرحلهای فعال نیست",
|
||||||
|
TWO_FACTOR_BACKUP_CODES_GENERATED_SUCCESSFULLY = "کدهای پشتیبان با موفقیت تولید شدند",
|
||||||
|
TWO_FACTOR_BACKUP_CODE_VERIFIED_SUCCESSFULLY = "کد پشتیبان با موفقیت تأیید شد",
|
||||||
|
TWO_FACTOR_BACKUP_CODE_VERIFIED_FAILED = "کد پشتیبان نامعتبر است",
|
||||||
|
TWO_FACTOR_TOKEN_REQUIRED = "کد احراز هویت دو مرحلهای الزامی است",
|
||||||
|
TWO_FACTOR_TOKEN_INVALID = "کد احراز هویت دو مرحلهای نامعتبر است",
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// guards/jwt-auth.guard.ts
|
||||||
|
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthEntityType, AuthRequest, JwtPayload } from './auth.guard';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminAuthGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext) {
|
||||||
|
const request = context.switchToHttp().getRequest<AuthRequest>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secret = this.configService.get<string>('JWT_SECRET');
|
||||||
|
const token = this.extractTokenFromHeader(request);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await this.jwtService.verifyAsync<JwtPayload>(token, {
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (payload.type !== AuthEntityType.ADMIN) {
|
||||||
|
throw new UnauthorizedException('Access denied. Admin rights required.');
|
||||||
|
}
|
||||||
|
// 💡 We're assigning the payload to the request object here
|
||||||
|
// so that we can access it in our route handlers
|
||||||
|
request['user'] = payload;
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('error in AuthGuard', err);
|
||||||
|
throw new UnauthorizedException('Invalid or expired token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// guards/jwt-auth.guard.ts
|
||||||
|
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
export enum AuthEntityType {
|
||||||
|
USER = 'user',
|
||||||
|
ADMIN = 'admin',
|
||||||
|
}
|
||||||
|
export interface AuthRequest extends Request {
|
||||||
|
user: {
|
||||||
|
sub: string; // userId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string; // The userId
|
||||||
|
type: AuthEntityType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext) {
|
||||||
|
const request = context.switchToHttp().getRequest<AuthRequest>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const secret = this.configService.get<string>('JWT_SECRET');
|
||||||
|
const token = this.extractTokenFromHeader(request);
|
||||||
|
if (!token) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await this.jwtService.verifyAsync<JwtPayload>(token, {
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
if (payload.type !== AuthEntityType.USER) {
|
||||||
|
throw new UnauthorizedException('Access denied. User rights required.');
|
||||||
|
}
|
||||||
|
// 💡 We're assigning the payload to the request object here
|
||||||
|
// so that we can access it in our route handlers
|
||||||
|
request['user'] = payload;
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.log('error in AuthGuard', err);
|
||||||
|
throw new UnauthorizedException('Invalid or expired token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private extractTokenFromHeader(request: Request): string | undefined {
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
|
return type === 'Bearer' ? token : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface PaginatedResult<T> {
|
||||||
|
data: T[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
@Catch(HttpException)
|
||||||
|
export class HttpErrorFilter implements ExceptionFilter {
|
||||||
|
catch(exception: HttpException, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
|
const status = exception.getStatus();
|
||||||
|
|
||||||
|
const exceptionResponse = exception.getResponse();
|
||||||
|
|
||||||
|
let messages: (string | object)[] = [];
|
||||||
|
if (typeof exceptionResponse === 'string') {
|
||||||
|
messages = [exceptionResponse];
|
||||||
|
} else if (typeof exceptionResponse === 'object' && exceptionResponse !== null && 'message' in exceptionResponse) {
|
||||||
|
messages = Array.isArray(exceptionResponse.message)
|
||||||
|
? (exceptionResponse.message as string[])
|
||||||
|
: [exceptionResponse.message as string];
|
||||||
|
} else messages = ['Unexpected error'];
|
||||||
|
|
||||||
|
const errorResponse = {
|
||||||
|
success: false,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
|
||||||
|
response.status(status).json(errorResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
map(data => ({
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ValueProvider } from '@nestjs/common';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MIKRO_ORM_QUERY_LOGGER } from '../constants';
|
||||||
|
|
||||||
|
export const mikroOrmQueryLoggerProvider: ValueProvider = {
|
||||||
|
provide: MIKRO_ORM_QUERY_LOGGER, //
|
||||||
|
useValue: new Logger('mikro-orm'),
|
||||||
|
};
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
import KeyvRedis from '@keyv/redis';
|
||||||
|
import Keyv from 'keyv';
|
||||||
|
import type { CacheModuleAsyncOptions } from '@nestjs/cache-manager';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Logger } from '@nestjs/common'; // 1. Import Logger
|
||||||
|
|
||||||
|
export function cacheConfig(): CacheModuleAsyncOptions {
|
||||||
|
// 2. Create a logger instance
|
||||||
|
const logger = new Logger('CacheConfig');
|
||||||
|
|
||||||
|
return {
|
||||||
|
isGlobal: true,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
const ttl = configService.get<number>('CACHE_TTL', 120);
|
||||||
|
const namespace = configService.get<string>('CACHE_NAMESPACE', 'app-cache');
|
||||||
|
const redisUri = configService.getOrThrow<string>('REDIS_URI');
|
||||||
|
|
||||||
|
// 3. Create the KeyvRedis store first
|
||||||
|
const keyvRedisStore = new KeyvRedis(redisUri);
|
||||||
|
|
||||||
|
// 4. Attach event listeners
|
||||||
|
keyvRedisStore.client.on('connect', () => {
|
||||||
|
logger.log('✅ Redis connected successfully.');
|
||||||
|
});
|
||||||
|
keyvRedisStore.client.on('error', (error) => {
|
||||||
|
logger.error('❌ Redis connection error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Pass the store to Keyv
|
||||||
|
const store = new Keyv({
|
||||||
|
store: keyvRedisStore, // Use the instance
|
||||||
|
namespace,
|
||||||
|
ttl: ttl * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
ttl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import type { HttpModuleAsyncOptions } from '@nestjs/axios';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
export function httpConfig(): HttpModuleAsyncOptions {
|
||||||
|
return {
|
||||||
|
inject: [ConfigService],
|
||||||
|
global: true,
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
return {
|
||||||
|
timeout: configService.getOrThrow<number>('AXIOS_TIMEOUT'),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
global: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { defineConfig, PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
// 1. Load environment variables from your .env file
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// 2. Read variables directly from process.env
|
||||||
|
const DB_PASS = process.env.DB_PASS;
|
||||||
|
const DB_USER = process.env.DB_USER;
|
||||||
|
const DB_HOST = process.env.DB_HOST;
|
||||||
|
const DB_PORT = process.env.DB_PORT ? +process.env.DB_PORT : 5432;
|
||||||
|
const DB_NAME = process.env.DB_NAME;
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
// 3. Add checks to ensure variables are loaded
|
||||||
|
if (!DB_PASS || !DB_USER || !DB_HOST || !DB_PORT || !DB_NAME) {
|
||||||
|
throw new Error('One or more database environment variables are not set.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedPassword = encodeURIComponent(DB_PASS);
|
||||||
|
|
||||||
|
// 4. Export the result of defineConfig as the default
|
||||||
|
export default defineConfig({
|
||||||
|
entities: ['dist/**/*.entity.js'],
|
||||||
|
entitiesTs: ['src/**/*.entity.ts'],
|
||||||
|
driver: PostgreSqlDriver,
|
||||||
|
dbName: DB_NAME,
|
||||||
|
debug: !isProduction,
|
||||||
|
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
|
||||||
|
ensureDatabase: isProduction
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
forceCheck: true,
|
||||||
|
create: true,
|
||||||
|
schema: 'update',
|
||||||
|
},
|
||||||
|
forceUtcTimezone: true,
|
||||||
|
pool: {
|
||||||
|
min: isProduction ? 5 : 2,
|
||||||
|
max: isProduction ? 20 : 10,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
acquireTimeoutMillis: 30000,
|
||||||
|
reapIntervalMillis: 1000,
|
||||||
|
createTimeoutMillis: 15000,
|
||||||
|
destroyTimeoutMillis: 5000,
|
||||||
|
},
|
||||||
|
// 5. Use console for CLI logging, not the injected Nest logger
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
logger: isProduction ? console.log.bind(console) : console.debug.bind(console),
|
||||||
|
schemaGenerator: {
|
||||||
|
disableForeignKeys: false,
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
path: './database/migrations',
|
||||||
|
pathTs: './database/migrations',
|
||||||
|
tableName: 'migrations',
|
||||||
|
transactional: true,
|
||||||
|
allOrNothing: true,
|
||||||
|
dropTables: false,
|
||||||
|
safe: true,
|
||||||
|
emit: 'ts',
|
||||||
|
},
|
||||||
|
connect: true,
|
||||||
|
allowGlobalContext: true,
|
||||||
|
driverOptions: {
|
||||||
|
connection: {
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveInitialDelayMillis: 10000,
|
||||||
|
statement_timeout: 60000,
|
||||||
|
query_timeout: 60000,
|
||||||
|
idle_in_transaction_session_timeout: 60000,
|
||||||
|
connectionTimeoutMillis: 10000,
|
||||||
|
application_name: DB_NAME,
|
||||||
|
},
|
||||||
|
connectionRetries: 5,
|
||||||
|
connectionRetryDelay: 3000,
|
||||||
|
validateConnection: (connection: any) => {
|
||||||
|
try {
|
||||||
|
return !!(connection && !connection.closed);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
// Use console.error directly
|
||||||
|
console.error(`Connection validation failed: ${(e as Error).message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
poolErrorHandler: (err: Error) => {
|
||||||
|
// Use console.error directly
|
||||||
|
console.error(`PostgreSQL pool error: ${err.message}`);
|
||||||
|
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
|
||||||
|
console.warn('Connection reset detected, will attempt to reconnect');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import type { MikroOrmModuleAsyncOptions } from '@mikro-orm/nestjs';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
|
||||||
|
// import { defineConfig } from '@mikro-orm/postgresql';
|
||||||
|
|
||||||
|
import type { Logger } from '@nestjs/common';
|
||||||
|
import { MIKRO_ORM_QUERY_LOGGER } from '../common/constants';
|
||||||
|
import { mikroOrmQueryLoggerProvider } from '../common/providers/mikro-orm-logger.provider';
|
||||||
|
|
||||||
|
const dataBaseConfig: MikroOrmModuleAsyncOptions = {
|
||||||
|
// entities: ['dist/**/*.entity.js'],
|
||||||
|
// entitiesTs: ['src/**/*.entity.ts'],
|
||||||
|
// dbName: 'your_db_name',
|
||||||
|
// type: 'postgresql',
|
||||||
|
// user: 'your_user',
|
||||||
|
// password: 'your_password',
|
||||||
|
// host: 'localhost',
|
||||||
|
// port: 5432,
|
||||||
|
// debug: true,
|
||||||
|
|
||||||
|
inject: [ConfigService, MIKRO_ORM_QUERY_LOGGER],
|
||||||
|
providers: [mikroOrmQueryLoggerProvider],
|
||||||
|
useFactory: (configService: ConfigService, logger: Logger) => {
|
||||||
|
const DB_PASS = configService.getOrThrow<string>('DB_PASS');
|
||||||
|
const DB_USER = configService.getOrThrow<string>('DB_USER');
|
||||||
|
const DB_HOST = configService.getOrThrow<string>('DB_HOST');
|
||||||
|
const DB_PORT = configService.getOrThrow<number>('DB_PORT');
|
||||||
|
const DB_NAME = configService.getOrThrow<string>('DB_NAME');
|
||||||
|
const encodedPassword = encodeURIComponent(DB_PASS);
|
||||||
|
const isProduction = configService.getOrThrow<string>('NODE_ENV') === 'production';
|
||||||
|
|
||||||
|
return ({
|
||||||
|
// entities: ['dist/**/*.entity.js'],
|
||||||
|
entitiesTs: ['src/**/*.entity.ts'],
|
||||||
|
driver: PostgreSqlDriver,
|
||||||
|
autoLoadEntities: true,
|
||||||
|
dbName: DB_NAME,
|
||||||
|
debug: !isProduction,
|
||||||
|
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
|
||||||
|
ensureDatabase: isProduction
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
forceCheck: true,
|
||||||
|
create: true,
|
||||||
|
schema: 'update',
|
||||||
|
},
|
||||||
|
forceUtcTimezone: true,
|
||||||
|
pool: {
|
||||||
|
min: isProduction ? 5 : 2,
|
||||||
|
max: isProduction ? 20 : 10,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
acquireTimeoutMillis: 30000,
|
||||||
|
reapIntervalMillis: 1000,
|
||||||
|
createTimeoutMillis: 15000,
|
||||||
|
destroyTimeoutMillis: 5000,
|
||||||
|
},
|
||||||
|
logger: isProduction ? message => logger.log(message) : message => logger.debug(message),
|
||||||
|
schemaGenerator: {
|
||||||
|
// createForeignKey: !isProduction,
|
||||||
|
disableForeignKeys: false,
|
||||||
|
// createIndex: true,
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
path: './database/migrations',
|
||||||
|
pathTs: './database/migrations',
|
||||||
|
tableName: 'migrations',
|
||||||
|
transactional: true,
|
||||||
|
allOrNothing: true,
|
||||||
|
dropTables: false,
|
||||||
|
safe: true,
|
||||||
|
emit: 'ts',
|
||||||
|
},
|
||||||
|
connect: true,
|
||||||
|
allowGlobalContext: true,
|
||||||
|
driverOptions: {
|
||||||
|
connection: {
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveInitialDelayMillis: 10000,
|
||||||
|
statement_timeout: 60000,
|
||||||
|
query_timeout: 60000,
|
||||||
|
idle_in_transaction_session_timeout: 60000,
|
||||||
|
connectionTimeoutMillis: 10000,
|
||||||
|
application_name: configService.getOrThrow<string>('DB_NAME'),
|
||||||
|
},
|
||||||
|
connectionRetries: 5,
|
||||||
|
connectionRetryDelay: 3000,
|
||||||
|
validateConnection: (connection: any) => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
return !!(connection && !connection.closed);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
logger.error(`Connection validation failed: ${(e as Error).message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
poolErrorHandler: (err: Error) => {
|
||||||
|
logger.error(`PostgreSQL pool error: ${err.message}`);
|
||||||
|
if (err.message.includes('ECONNRESET') || err.message.includes('Connection terminated unexpectedly')) {
|
||||||
|
logger.warn('Connection reset detected, will attempt to reconnect');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dataBaseConfig;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { INestApplication } from '@nestjs/common';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export function getSwaggerConfig(app: INestApplication) {
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Tahavol API')
|
||||||
|
.setDescription('API documentation for Tahavol backend')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addBearerAuth() // optional: for JWT endpoints
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
|
||||||
|
SwaggerModule.setup('docs', app, document, {
|
||||||
|
swaggerOptions: {
|
||||||
|
persistAuthorization: true,
|
||||||
|
displayRequestDuration: true,
|
||||||
|
filter: true,
|
||||||
|
showExtensions: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||||
|
import { getSwaggerConfig } from './config/swagger.config';
|
||||||
|
import multipart from '@fastify/multipart';
|
||||||
|
|
||||||
|
// 👈 Import the Fastify application type
|
||||||
|
import { type NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const logger = new Logger('APP');
|
||||||
|
|
||||||
|
// 1. Create the application using the Fastify adapter
|
||||||
|
const app = await NestFactory.create<NestFastifyApplication>(
|
||||||
|
AppModule,
|
||||||
|
new FastifyAdapter(), // Instantiate the FastifyAdapter
|
||||||
|
// Optional: Pass the custom logger (like your 'APP' logger) to NestFactory
|
||||||
|
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] },
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.register(multipart);
|
||||||
|
app.useGlobalPipes(new ValidationPipe());
|
||||||
|
|
||||||
|
const configService = app.get<ConfigService>(ConfigService);
|
||||||
|
// Ensure the port is handled correctly, use 4000 as fallback if needed
|
||||||
|
const APP_PORT = configService.getOrThrow<number>('APP_PORT') ?? 4000;
|
||||||
|
|
||||||
|
// 2. Swagger Integration Note
|
||||||
|
// Since you are using platform-fastify, you must ensure your getSwaggerConfig
|
||||||
|
// function is configured to use the Fastify platform option if necessary.
|
||||||
|
getSwaggerConfig(app);
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
origin: true,
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
|
credentials: true,
|
||||||
|
optionsSuccessStatus: 204,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Listen on port using Fastify's listen method
|
||||||
|
// '0.0.0.0' is recommended for Docker/containerized environments
|
||||||
|
await app.listen(APP_PORT, '0.0.0.0', () => {
|
||||||
|
logger.log(`Application is running on: http://localhost:${APP_PORT}`);
|
||||||
|
logger.log(`Swagger documentation: http://localhost:${APP_PORT}/docs`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AdminService } from './admin.service';
|
||||||
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
|
import { Admin } from './entities/user.entity';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [AdminService],
|
||||||
|
controllers: [],
|
||||||
|
imports: [MikroOrmModule.forFeature([Admin]), JwtModule],
|
||||||
|
exports: [AdminService],
|
||||||
|
})
|
||||||
|
export class AdminModule {}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||||
|
import { EntityRepository } from '@mikro-orm/core';
|
||||||
|
import { Admin } from './entities/user.entity';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Admin)
|
||||||
|
private readonly adminRepository: EntityRepository<Admin>,
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findByPhone(phone: string): Promise<Admin | null> {
|
||||||
|
return this.adminRepository.findOne({ phone });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: number): Promise<Admin | null> {
|
||||||
|
return this.adminRepository.findOne({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(phone: string): Promise<Admin> {
|
||||||
|
const user = this.adminRepository.create({ phone });
|
||||||
|
await this.em.persistAndFlush(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Entity, PrimaryKey, Property, OptionalProps } from '@mikro-orm/core';
|
||||||
|
|
||||||
|
@Entity({ tableName: 'admns' })
|
||||||
|
export class Admin {
|
||||||
|
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt';
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@Property({ unique: true })
|
||||||
|
phone!: string;
|
||||||
|
|
||||||
|
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@Property({ onUpdate: () => new Date(), defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||||
|
updatedAt: Date = new Date();
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Controller, Post, Body } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { RequestOtpDto } from './dto/request-otp.dto';
|
||||||
|
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { VerifyOtpDto } from './dto/verify-otp.dto copy';
|
||||||
|
import { Throttle } from '@nestjs/throttler';
|
||||||
|
|
||||||
|
@ApiTags('auth')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Throttle({ default: { limit: 3, ttl: 180_000 } })
|
||||||
|
@Post('otp/request')
|
||||||
|
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||||
|
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||||
|
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
||||||
|
otpRequest(@Body() dto: RequestOtpDto) {
|
||||||
|
return this.authService.requestOtp(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('otp/verify')
|
||||||
|
@ApiOperation({ summary: 'Verify OTP code' })
|
||||||
|
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||||
|
@ApiResponse({ status: 200, description: 'OTP verified successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Invalid OTP or expired' })
|
||||||
|
otpVerify(@Body() dto: VerifyOtpDto) {
|
||||||
|
return this.authService.verifyOtp(dto.phone, dto.otp); // assuming dto has `otp` property
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('admin/otp/request')
|
||||||
|
@ApiOperation({ summary: 'Request OTP for login or signup' })
|
||||||
|
@ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
||||||
|
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
||||||
|
adminOtpRequest(@Body() dto: RequestOtpDto) {
|
||||||
|
return this.authService.requestOtpAdmin(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('admin/otp/verify')
|
||||||
|
@ApiOperation({ summary: 'Verify OTP code' })
|
||||||
|
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
||||||
|
@ApiResponse({ status: 200, description: 'OTP verified successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Invalid OTP or expired' })
|
||||||
|
adminOtpVerify(@Body() dto: VerifyOtpDto) {
|
||||||
|
return this.authService.verifyOtpAdmin(dto.phone, dto.otp); // assuming dto has `otp` property
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { UtilsModule } from '../utils/utils.module';
|
||||||
|
import { UserModule } from '../user/user.module';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AdminModule } from '../admin/admin.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
UtilsModule,
|
||||||
|
UserModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
global: true,
|
||||||
|
secret: configService.getOrThrow<string>('JWT_SECRET'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.getOrThrow<number>('JWT_EXPIRATION_TIME'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
AdminModule,
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Injectable, BadRequestException, UnauthorizedException, NotFoundException } from '@nestjs/common';
|
||||||
|
import { RequestOtpDto } from './dto/request-otp.dto';
|
||||||
|
import { CacheService } from '../utils/cache.service';
|
||||||
|
import { SmsService } from '../utils/sms.service';
|
||||||
|
import { UserService } from '../user/user.service';
|
||||||
|
import { randomInt } from 'crypto';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { AdminService } from '../admin/admin.service';
|
||||||
|
import { AuthEntityType } from 'src/common/guards/auth.guard';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly cacheService: CacheService,
|
||||||
|
private readonly smsService: SmsService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly adminService: AdminService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async requestOtp(dto: RequestOtpDto) {
|
||||||
|
const { phone } = dto;
|
||||||
|
const code = this.generateOtpCode();
|
||||||
|
|
||||||
|
await this.cacheService.set(`otp:${phone}`, code, 160);
|
||||||
|
|
||||||
|
await this.smsService.sendOtp(phone, code);
|
||||||
|
|
||||||
|
return { success: true, message: ' OTP sent successfully' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestOtpAdmin(dto: RequestOtpDto) {
|
||||||
|
const { phone } = dto;
|
||||||
|
|
||||||
|
const admin = await this.adminService.findByPhone(phone);
|
||||||
|
if (!admin) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = this.generateOtpCode();
|
||||||
|
|
||||||
|
await this.cacheService.set(`otp-admin:${phone}`, code, 160);
|
||||||
|
|
||||||
|
await this.smsService.sendOtp(phone, code);
|
||||||
|
|
||||||
|
return { success: true, message: ' OTP sent successfully' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOtp(phone: string, code: string) {
|
||||||
|
const cachedCode = await this.cacheService.get(`otp:${phone}`);
|
||||||
|
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||||
|
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||||
|
|
||||||
|
await this.cacheService.del(`otp:${phone}`);
|
||||||
|
|
||||||
|
const user = await this.userService.findOrCreateByPhone(phone);
|
||||||
|
|
||||||
|
const accessToken = await this.issueToken(user.id.toString(), AuthEntityType.USER);
|
||||||
|
|
||||||
|
return { success: true, message: 'User registered successfully!', accessToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOtpAdmin(phone: string, code: string) {
|
||||||
|
const cachedCode = await this.cacheService.get(`otp-admin:${phone}`);
|
||||||
|
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||||
|
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||||
|
|
||||||
|
await this.cacheService.del(`otp:${phone}`);
|
||||||
|
|
||||||
|
const admin = await this.adminService.findByPhone(phone);
|
||||||
|
if (!admin) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
const accessToken = await this.issueToken(admin.id.toString(), AuthEntityType.ADMIN);
|
||||||
|
|
||||||
|
return { success: true, message: 'successfully!', accessToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateOtpCode(): string {
|
||||||
|
const code = randomInt(10000, 100000);
|
||||||
|
return code.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private issueToken(id: string, entityType: AuthEntityType) {
|
||||||
|
const payload = {
|
||||||
|
sub: id,
|
||||||
|
type: entityType,
|
||||||
|
};
|
||||||
|
return this.jwtService.signAsync(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsNotEmpty, IsString, Matches } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class RequestOtpDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||||
|
@Matches(/^09\d{9}$/, {
|
||||||
|
message: 'Mobile number must be a valid Iranian phone number (e.g., 09123456789)',
|
||||||
|
})
|
||||||
|
// @IsMobilePhone('fa-IR')
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { IsNotEmpty, IsString, Length, Matches } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class VerifyOtpDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||||
|
@Matches(/^09\d{9}$/, {
|
||||||
|
message: 'Mobile number must be a valid Iranian phone number (e.g., 09123456789)',
|
||||||
|
})
|
||||||
|
// @IsMobilePhone('fa-IR')
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
@ApiProperty({ example: '12345', description: 'Otp' })
|
||||||
|
@Length(5)
|
||||||
|
otp: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { IsString, IsNotEmpty, IsDate, IsOptional } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateSubmissioncDto {
|
||||||
|
@ApiPropertyOptional({ example: 'صداو سیما', description: 'دستگاه همکار : مثلا صدا و سیما' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
cooperator: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'ملی', description: 'ملی|استانی|' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
cooperationLevel: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '2025-11-03T12:30:00.000Z',
|
||||||
|
description: 'The date and time of the submission (ISO 8601 format).',
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Submission Date is required.' })
|
||||||
|
@IsDate({ message: 'Submission Date must be a valid ISO 8601 date.' })
|
||||||
|
@Type(() => Date)
|
||||||
|
submissionDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'مقاله', description: ' ' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
submissionType: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, Min, IsEnum, IsIn } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Submition, SubmitionStatus } from '../entities/submition.entity';
|
||||||
|
|
||||||
|
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||||
|
type SortOrder = (typeof sortOrderOptions)[number];
|
||||||
|
|
||||||
|
export class FindSubmitionsDto {
|
||||||
|
/**
|
||||||
|
* The page number to retrieve.
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The page number to retrieve.',
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
minimum: 1,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of items per page.
|
||||||
|
* @default 10
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The number of items per page.',
|
||||||
|
type: Number,
|
||||||
|
default: 10,
|
||||||
|
minimum: 1,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A general search term to filter by.
|
||||||
|
* Searches firstName, lastName, phone, and nationalCode.
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'A general search term (firstName, lastName, phone, nationalCode).',
|
||||||
|
type: String,
|
||||||
|
example: 'John Doe',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by a specific user status.
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Filter by a specific user status.',
|
||||||
|
enum: SubmitionStatus, // Use the actual enum
|
||||||
|
example: SubmitionStatus.finished,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(SubmitionStatus)
|
||||||
|
status?: SubmitionStatus;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Filter by a cooperationLevel.',
|
||||||
|
example: 'ملی',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
cooperationLevel?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Filter by a submissionType.',
|
||||||
|
example: 'مقاله',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
submissionType?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The field to sort the results by.
|
||||||
|
* @default "user"
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The field to sort the results by.',
|
||||||
|
type: String,
|
||||||
|
default: 'user',
|
||||||
|
example: 'user',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
orderBy?: keyof Submition = 'user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction to sort the results.
|
||||||
|
* @default "desc"
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The direction to sort the results.',
|
||||||
|
enum: sortOrderOptions, // Use the constant array
|
||||||
|
default: 'desc',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(sortOrderOptions)
|
||||||
|
order?: SortOrder = 'desc';
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsArray,
|
||||||
|
ArrayMinSize,
|
||||||
|
Validate,
|
||||||
|
ValidatorConstraint,
|
||||||
|
ValidatorConstraintInterface,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ValidatorConstraint({ name: 'MaxWordCount', async: false })
|
||||||
|
export class MaxWordCount implements ValidatorConstraintInterface {
|
||||||
|
validate(text: string) {
|
||||||
|
if (typeof text !== 'string') return false;
|
||||||
|
const wordCount = text.trim().split(/\s+/).filter(Boolean).length;
|
||||||
|
return wordCount <= 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage() {
|
||||||
|
return 'Submission description must not exceed 1000 words.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateDocsDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: 'توضیحاتی در مورد مدارک ارسالی و هدف از ارسال.',
|
||||||
|
description: 'Detailed description or notes regarding the documents being submitted (maximum 1000 words).',
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Submission description is required.' })
|
||||||
|
@IsString({ message: 'Submission description must be a string.' })
|
||||||
|
@Validate(MaxWordCount)
|
||||||
|
submissionDesc: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: ['https://example.com/doc1.pdf', 'https://example.com/doc2.jpg'],
|
||||||
|
description: 'An array of URLs pointing to the submitted documents.',
|
||||||
|
type: [String],
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Document URLs are required.' })
|
||||||
|
@IsArray({ message: 'Document URLs must be an array.' })
|
||||||
|
@ArrayMinSize(1, { message: 'At least one document URL is required.' })
|
||||||
|
@IsString({ each: true, message: 'Each document URL must be a valid string.' })
|
||||||
|
docsUrl: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { IsString, IsNotEmpty, IsDate, IsOptional } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class UpdateSubmissioncDto {
|
||||||
|
@ApiPropertyOptional({ example: 'صداو سیما', description: 'دستگاه همکار : مثلا صدا و سیما' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
cooperator: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'ملی', description: 'ملی|استانی|' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
cooperationLevel: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '2025-11-03T12:30:00.000Z',
|
||||||
|
description: 'The date and time of the submission (ISO 8601 format).',
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Submission Date is required.' })
|
||||||
|
@IsDate({ message: 'Submission Date must be a valid ISO 8601 date.' })
|
||||||
|
@Type(() => Date)
|
||||||
|
submissionDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'مقاله', description: ' ' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
submissionType: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Entity, Enum, PrimaryKey, Property, OptionalProps, ManyToOne } from '@mikro-orm/core';
|
||||||
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
|
||||||
|
export enum SubmitionStatus {
|
||||||
|
finished = 'تکمیل',
|
||||||
|
pending = 'ناقص',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ tableName: 'submition' })
|
||||||
|
export class Submition {
|
||||||
|
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt';
|
||||||
|
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
user!: User;
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Enum({ items: () => SubmitionStatus, nullable: true, default: SubmitionStatus.pending })
|
||||||
|
status?: SubmitionStatus;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
submissionType?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
cooperator?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
cooperationLevel?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true, columnType: 'text' })
|
||||||
|
submissionDesc?: string;
|
||||||
|
|
||||||
|
@Property({ type: 'text[]', nullable: true })
|
||||||
|
docsUrl?: string[];
|
||||||
|
|
||||||
|
@Property({ columnType: 'timestamptz', nullable: true })
|
||||||
|
submissionDate?: Date;
|
||||||
|
|
||||||
|
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@Property({ onUpdate: () => new Date(), defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||||
|
updatedAt: Date = new Date();
|
||||||
|
|
||||||
|
@Property({ nullable: true, columnType: 'timestamptz' })
|
||||||
|
deletedAt?: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Req,
|
||||||
|
UseGuards,
|
||||||
|
Query,
|
||||||
|
ValidationPipe,
|
||||||
|
Post,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Delete,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiParam } from '@nestjs/swagger';
|
||||||
|
import { AuthGuard, type AuthRequest } from 'src/common/guards/auth.guard';
|
||||||
|
import { SubmitionService } from './submition.service';
|
||||||
|
import { UpdateSubmissioncDto } from './dto/update-submission.dto';
|
||||||
|
import { UpdateDocsDto } from './dto/update-docs.dto';
|
||||||
|
import { AdminAuthGuard } from 'src/common/guards/auth.admin.guard';
|
||||||
|
import { FindSubmitionsDto } from './dto/find-submitions.dto';
|
||||||
|
import { CreateSubmissioncDto } from './dto/create-submission.dto';
|
||||||
|
import { SubmitionStatus } from './entities/submition.entity';
|
||||||
|
|
||||||
|
@ApiTags('submition')
|
||||||
|
@Controller('submition')
|
||||||
|
export class SubmitionController {
|
||||||
|
constructor(private readonly submitionService: SubmitionService) {}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Get User submissions' })
|
||||||
|
@Get('/')
|
||||||
|
async getUserSbmitions(@Req() req: AuthRequest) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const submitions = await this.submitionService.findByUserId(+userId);
|
||||||
|
return {
|
||||||
|
message: `User submissions `,
|
||||||
|
submitions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Get single submission' })
|
||||||
|
@ApiParam({ name: 'id', description: 'The ID of the submission ', type: Number })
|
||||||
|
@Get(':id')
|
||||||
|
async getSbmition(@Req() req: AuthRequest, @Param('id', ParseIntPipe) id: number) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const submition = await this.submitionService.findById(id, +userId);
|
||||||
|
return {
|
||||||
|
message: `Success `,
|
||||||
|
submition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Create submission :first step of creation' })
|
||||||
|
@ApiBody({ type: CreateSubmissioncDto })
|
||||||
|
@Post('/')
|
||||||
|
async createSubmition(@Req() req: AuthRequest, @Body() dto: CreateSubmissioncDto) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const submition = await this.submitionService.create(+userId, dto);
|
||||||
|
return {
|
||||||
|
message: `Created submission `,
|
||||||
|
userId,
|
||||||
|
submition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiParam({ name: 'id', description: 'The ID of the submission to update', type: Number })
|
||||||
|
@ApiOperation({ summary: 'Update the submission : first step of update' })
|
||||||
|
@ApiBody({ type: UpdateSubmissioncDto })
|
||||||
|
@Patch(':id')
|
||||||
|
async updateUserSubmissionType(
|
||||||
|
@Req() req: AuthRequest,
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body() dto: UpdateSubmissioncDto,
|
||||||
|
) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const submition = await this.submitionService.updateSubmition(id, +userId, dto);
|
||||||
|
return {
|
||||||
|
message: `PATCH request: Updating submission type for user done`,
|
||||||
|
userId,
|
||||||
|
submition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiParam({ name: 'id', description: 'The ID of the submission to update', type: Number })
|
||||||
|
@ApiOperation({ summary: 'Update the user document/file reference' })
|
||||||
|
@ApiBody({ type: UpdateDocsDto })
|
||||||
|
@Patch(':id/doc')
|
||||||
|
async updateUserDoc(@Req() req: AuthRequest, @Param('id', ParseIntPipe) id: number, @Body() dto: UpdateDocsDto) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const submition = await this.submitionService.updateSubmition(id, +userId, {
|
||||||
|
...dto,
|
||||||
|
status: SubmitionStatus.finished,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: `PATCH request: Updating doc field for user ${userId} `,
|
||||||
|
userId,
|
||||||
|
submition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'submissions' })
|
||||||
|
@Get('/admin/submissions')
|
||||||
|
async findAll(
|
||||||
|
@Query(new ValidationPipe({ transform: true, whitelist: true }))
|
||||||
|
query: FindSubmitionsDto,
|
||||||
|
) {
|
||||||
|
return this.submitionService.findAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Delete submission' })
|
||||||
|
@ApiParam({ name: 'id', description: 'The ID of the submission ', type: Number })
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteSbmition(@Req() req: AuthRequest, @Param('id', ParseIntPipe) id: number) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const submition = await this.submitionService.deleteSubmition(id, +userId);
|
||||||
|
return {
|
||||||
|
message: `submition deleted successfully `,
|
||||||
|
submition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SubmitionService } from './submition.service';
|
||||||
|
import { SubmitionController } from './submition.controller';
|
||||||
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { Submition } from './entities/submition.entity';
|
||||||
|
// import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [SubmitionService],
|
||||||
|
controllers: [SubmitionController],
|
||||||
|
imports: [MikroOrmModule.forFeature([Submition]), JwtModule],
|
||||||
|
exports: [SubmitionService],
|
||||||
|
})
|
||||||
|
export class SubmitionModule {}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||||
|
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||||
|
import { SubmitionStatus, Submition } from './entities/submition.entity';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { UpdateSubmissioncDto } from './dto/update-submission.dto';
|
||||||
|
import { UpdateDocsDto } from './dto/update-docs.dto';
|
||||||
|
import { FindSubmitionsDto } from './dto/find-submitions.dto';
|
||||||
|
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||||
|
import { CreateSubmissioncDto } from './dto/create-submission.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubmitionService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Submition)
|
||||||
|
private readonly submitionRepository: EntityRepository<Submition>,
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async updateSubmition(
|
||||||
|
submitionId: number,
|
||||||
|
userId: number,
|
||||||
|
dto: UpdateSubmissioncDto | (UpdateDocsDto & { status: SubmitionStatus }),
|
||||||
|
): Promise<Submition> {
|
||||||
|
const sub = await this.em.findOneOrFail(Submition, submitionId, { populate: ['user'] }).catch(() => {
|
||||||
|
throw new NotFoundException(`submition with ID ${submitionId} not found.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sub.user.id !== userId) {
|
||||||
|
throw new ForbiddenException(`User is not the owner of submition ID ${submitionId}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.em.assign(sub, dto);
|
||||||
|
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(submitionId: number, userId: number): Promise<Submition | null> {
|
||||||
|
return this.submitionRepository.findOne({ id: submitionId, user: { id: userId }, deletedAt: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUserId(userId: number): Promise<Submition[] | null> {
|
||||||
|
return this.submitionRepository.find({ user: userId, deletedAt: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(userId: number, dto: CreateSubmissioncDto): Promise<Submition> {
|
||||||
|
const submition = this.submitionRepository.create({ ...dto, user: userId });
|
||||||
|
await this.em.persistAndFlush(submition);
|
||||||
|
return submition;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(dto: FindSubmitionsDto): Promise<PaginatedResult<Submition>> {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
search,
|
||||||
|
status,
|
||||||
|
cooperationLevel,
|
||||||
|
submissionType,
|
||||||
|
orderBy = 'id',
|
||||||
|
order = 'desc',
|
||||||
|
} = dto;
|
||||||
|
|
||||||
|
// 1. Calculate pagination
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// 2. Build the 'where' filter query
|
||||||
|
const where: FilterQuery<Submition> = {
|
||||||
|
deletedAt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Add exact-match filters (if provided)
|
||||||
|
if (status) {
|
||||||
|
where.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submissionType) {
|
||||||
|
where.submissionType = submissionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cooperationLevel) {
|
||||||
|
where.cooperationLevel = cooperationLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Add 'search' logic (case-insensitive)
|
||||||
|
if (search) {
|
||||||
|
const searchPattern = `%${search}%`;
|
||||||
|
where.$or = [
|
||||||
|
{ submissionDesc: { $ilike: searchPattern } },
|
||||||
|
{ cooperator: { $ilike: searchPattern } },
|
||||||
|
{ user: { firstName: { $ilike: searchPattern } } },
|
||||||
|
{ user: { lastName: { $ilike: searchPattern } } },
|
||||||
|
{ user: { nationalCode: { $ilike: searchPattern } } },
|
||||||
|
{ user: { personalCode: { $ilike: searchPattern } } },
|
||||||
|
{ user: { phone: { $ilike: searchPattern } } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Execute the query using findAndCount
|
||||||
|
const [sumitions, total] = await this.submitionRepository.findAndCount(where, {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||||
|
populate: ['user'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Calculate total pages
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
// 7. Return the paginated result
|
||||||
|
return {
|
||||||
|
data: sumitions,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSubmition(submitionId: number, userId: number): Promise<Submition> {
|
||||||
|
const submission = await this.em.findOneOrFail(Submition, submitionId, { populate: ['user'] }).catch(() => {
|
||||||
|
throw new NotFoundException(`Submition with ID ${submitionId} not found.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ownership Check (same as before)
|
||||||
|
if (submission.user.id !== userId) {
|
||||||
|
throw new ForbiddenException(`User with ID ${userId} is not authorized to delete submition ID ${submitionId}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the Soft Delete: Update the timestamp
|
||||||
|
submission.deletedAt = new Date();
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return submission;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import type { File } from '@nest-lab/fastify-multer';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class UploadSingleFileDto {
|
||||||
|
@ApiProperty({ type: 'string', format: 'binary', nullable: false })
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UploadMultipleFileDto {
|
||||||
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
files: File[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { File } from "@nest-lab/fastify-multer";
|
||||||
|
|
||||||
|
export interface FileUploadResult {
|
||||||
|
file: File;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileStreamResult {
|
||||||
|
stream: NodeJS.ReadableStream;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileMetadata {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
duration?: number;
|
||||||
|
bitrate?: number;
|
||||||
|
codec?: string;
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileType = "video" | "audio" | "image" | "document" | "text" | "unknown";
|
||||||
|
|
||||||
|
export interface FileValidationOptions {
|
||||||
|
maxFileSize: number;
|
||||||
|
allowedMimeTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadOptions {
|
||||||
|
roomId?: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File processing status
|
||||||
|
export type FileProcessingStatus = "pending" | "processing" | "completed" | "failed";
|
||||||
|
|
||||||
|
export interface FileProcessingResult {
|
||||||
|
status: FileProcessingStatus;
|
||||||
|
metadata?: FileMetadata;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export interface S3UploadResult {
|
||||||
|
key: string;
|
||||||
|
url: string;
|
||||||
|
bucket: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3UploadOptions {
|
||||||
|
contentType: string;
|
||||||
|
metadata?: Record<string, string>;
|
||||||
|
acl?: "private" | "public-read" | "public-read-write";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3DownloadOptions {
|
||||||
|
expiresIn?: number;
|
||||||
|
responseContentType?: string;
|
||||||
|
responseContentDisposition?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3FileInfo {
|
||||||
|
key: string;
|
||||||
|
bucket: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: Date;
|
||||||
|
contentType: string;
|
||||||
|
etag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3DeleteResult {
|
||||||
|
deleted: boolean;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface S3ListResult {
|
||||||
|
files: S3FileInfo[];
|
||||||
|
continuationToken?: string;
|
||||||
|
isTruncated: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { randomInt } from 'node:crypto';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
|
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||||
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { FileType } from '../interfaces/file.interface';
|
||||||
|
import { S3UploadResult } from '../interfaces/s3.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class S3Service {
|
||||||
|
private readonly logger = new Logger(S3Service.name);
|
||||||
|
private readonly s3Client: S3Client;
|
||||||
|
private readonly bucketName: string;
|
||||||
|
private readonly region: string; // Region is often 'us-east-1' or similar for Liara, but depends on your config
|
||||||
|
private readonly endpointUrl: string; // Renamed 'url' to 'endpointUrl' for clarity
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
// --- Configuration for Liara S3 ---
|
||||||
|
this.bucketName = this.configService.getOrThrow<string>('BUCKET_NAME');
|
||||||
|
// Liara S3 usually requires a specific endpoint URL
|
||||||
|
this.endpointUrl = this.configService.getOrThrow<string>('LIARA_S3_ENDPOINT');
|
||||||
|
// The region for Liara S3 is often 'us-east-1' or just a placeholder,
|
||||||
|
// but we'll keep it configurable for consistency.
|
||||||
|
this.region = this.configService.getOrThrow<string>('BUCKET_REGION');
|
||||||
|
|
||||||
|
this.s3Client = new S3Client({
|
||||||
|
region: this.region, // Use the configured region
|
||||||
|
endpoint: this.endpointUrl, // This is the crucial change for Liara S3
|
||||||
|
forcePathStyle: true, // Often required when using custom S3 endpoints like Liara
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: this.configService.getOrThrow<string>('BUCKET_ACCESS_KEY'),
|
||||||
|
secretAccessKey: this.configService.getOrThrow<string>('BUCKET_SECRET_KEY'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// ------------------------------------
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload file to S3
|
||||||
|
*/
|
||||||
|
async uploadFile(
|
||||||
|
buffer: Buffer,
|
||||||
|
key: string,
|
||||||
|
contentType: string,
|
||||||
|
metadata?: Record<string, string>,
|
||||||
|
): Promise<S3UploadResult> {
|
||||||
|
try {
|
||||||
|
const sanitizedMetadata = metadata
|
||||||
|
? Object.entries(metadata).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
// Encode all values to be ASCII-safe
|
||||||
|
acc[key] = encodeURIComponent(value);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
// NOTE: 'ACL: public-read' might not be supported or necessary for Liara S3,
|
||||||
|
// depending on your bucket configuration on Liara.
|
||||||
|
// If uploads fail, try removing this line.
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: key,
|
||||||
|
Body: buffer,
|
||||||
|
// ACL: 'public-read',
|
||||||
|
ContentType: contentType,
|
||||||
|
Metadata: sanitizedMetadata,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.s3Client.send(command);
|
||||||
|
|
||||||
|
// --- URL for Liara S3 ---
|
||||||
|
// The public URL for Liara S3 files is typically: ${endpoint}/${bucketName}/${key}
|
||||||
|
const url = `${this.endpointUrl}/${this.bucketName}/${key}`;
|
||||||
|
|
||||||
|
this.logger.log(`File uploaded to S3: ${key}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
url,
|
||||||
|
bucket: this.bucketName,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Other methods remain the same as they use the S3Client which is now configured for Liara ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file stream from S3
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getFileStream(key: string): Promise<Readable> {
|
||||||
|
try {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.s3Client.send(command);
|
||||||
|
|
||||||
|
// ✅ VALIDATION
|
||||||
|
if (response.Body instanceof Readable) {
|
||||||
|
return response.Body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a Readable stream, throw a specific error
|
||||||
|
throw new Error(`File body is not a readable stream for key: ${key}`);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Generate presigned URL for file download
|
||||||
|
*/
|
||||||
|
async getPresignedUrl(key: string, expiresIn: number = 3600): Promise<string> {
|
||||||
|
try {
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await getSignedUrl(this.s3Client, command, { expiresIn });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete file from S3
|
||||||
|
*/
|
||||||
|
async deleteFile(key: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const command = new DeleteObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.s3Client.send(command);
|
||||||
|
this.logger.log(`File deleted from S3: ${key}`);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate S3 key for file
|
||||||
|
*/
|
||||||
|
generateFileKey(originalName: string, fileType: FileType): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomSuffix = randomInt(1000000, 9999999);
|
||||||
|
const extension = originalName.split('.').pop();
|
||||||
|
|
||||||
|
const basePathMap: Record<FileType, string> = {
|
||||||
|
image: 'images',
|
||||||
|
video: 'videos',
|
||||||
|
audio: 'audio',
|
||||||
|
document: 'documents',
|
||||||
|
text: 'texts',
|
||||||
|
unknown: 'unknowns',
|
||||||
|
};
|
||||||
|
|
||||||
|
const basePath = basePathMap[fileType];
|
||||||
|
return `${basePath}/${timestamp}-${randomSuffix}.${extension}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { File } from '@nest-lab/fastify-multer';
|
||||||
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
import { S3Service } from './s3.service';
|
||||||
|
import { UploaderMessage } from '../../../common/enums/message.enum';
|
||||||
|
import { FileType } from '../interfaces/file.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UploaderService {
|
||||||
|
private readonly logger = new Logger(UploaderService.name);
|
||||||
|
private readonly maxFileSize: number;
|
||||||
|
private readonly allowedMimeTypes: string[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly s3Service: S3Service,
|
||||||
|
) {
|
||||||
|
this.maxFileSize = this.configService.get('MAX_FILE_SIZE', 1024 * 1024 * 100); // 100MB
|
||||||
|
this.allowedMimeTypes = [
|
||||||
|
'video/mp4',
|
||||||
|
'video/webm',
|
||||||
|
'video/ogg',
|
||||||
|
'video/avi',
|
||||||
|
'video/mov',
|
||||||
|
'video/mkv',
|
||||||
|
'audio/mp3',
|
||||||
|
'audio/wav',
|
||||||
|
'audio/ogg',
|
||||||
|
'audio/webm',
|
||||||
|
'audio/m4a',
|
||||||
|
'audio/aac',
|
||||||
|
'audio/flac',
|
||||||
|
'audio/m4a',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/webp',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/tiff',
|
||||||
|
'image/bmp',
|
||||||
|
'application/pdf',
|
||||||
|
'text/plain',
|
||||||
|
'text/csv',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||||
|
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
||||||
|
'application/vnd.ms-excel.template.macroEnabled.12',
|
||||||
|
'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
||||||
|
'application/vnd.ms-excel.addin.macroEnabled.12',
|
||||||
|
'application/vnd.ms-powerpoint.addin.macroEnabled.12',
|
||||||
|
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
||||||
|
'application/vnd.ms-powerpoint.presentation.binary.macroEnabled.12',
|
||||||
|
'application/vnd.ms-excel.template.binary.macroEnabled.12',
|
||||||
|
'application/vnd.ms-powerpoint.template.binary.macroEnabled.12',
|
||||||
|
'application/vnd.ms-excel.addin.binary.macroEnabled.12',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadMultiple(files: File[]) {
|
||||||
|
return await Promise.all(files.map(file => this.uploadFile(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle file upload
|
||||||
|
*/
|
||||||
|
async uploadFile(file: File) {
|
||||||
|
if (!file || !file.buffer || !file.size) {
|
||||||
|
throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
|
||||||
|
}
|
||||||
|
this.logger.log(`Uploading file: ${file.originalname}`);
|
||||||
|
|
||||||
|
this.validateFile(file);
|
||||||
|
|
||||||
|
const fileType = await this.determineFileType(file.mimetype);
|
||||||
|
|
||||||
|
const s3Key = this.s3Service.generateFileKey(file.originalname, fileType);
|
||||||
|
|
||||||
|
// Upload to S3
|
||||||
|
const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, {
|
||||||
|
originalName: file.originalname,
|
||||||
|
uploadedBy: 'admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`File uploaded successfully: `);
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: s3Result,
|
||||||
|
url: s3Result.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate uploaded file
|
||||||
|
*/
|
||||||
|
private validateFile(file: File): void {
|
||||||
|
if (!file) throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID);
|
||||||
|
|
||||||
|
if (file.size && file.size > this.maxFileSize)
|
||||||
|
throw new BadRequestException(
|
||||||
|
UploaderMessage.UPLOAD_FILE_TOO_LARGE.replace('[MAX_FILE_SIZE]', this.maxFileSize.toString()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.allowedMimeTypes.includes(file.mimetype))
|
||||||
|
throw new BadRequestException(
|
||||||
|
UploaderMessage.UPLOAD_FILE_TYPE_NOT_SUPPORTED.replace('[MIME_TYPES]', file.mimetype),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine file type based on mimetype
|
||||||
|
*/
|
||||||
|
private async determineFileType(mimetype: string): Promise<FileType> {
|
||||||
|
if (mimetype.startsWith('video/')) {
|
||||||
|
return 'video';
|
||||||
|
} else if (mimetype.startsWith('audio/')) {
|
||||||
|
return 'audio';
|
||||||
|
} else if (mimetype.startsWith('image/')) {
|
||||||
|
return 'image';
|
||||||
|
} else if (mimetype.startsWith('text/')) {
|
||||||
|
return 'text';
|
||||||
|
} else if (mimetype.startsWith('application/')) {
|
||||||
|
return 'document';
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { type File, FileInterceptor, FilesInterceptor } from '@nest-lab/fastify-multer';
|
||||||
|
import { Controller, Post, UploadedFile, UploadedFiles, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||||
|
import { ApiBody, ApiConsumes, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
import { UploadMultipleFileDto, UploadSingleFileDto } from './DTO/upload-file.dto';
|
||||||
|
import { UploaderService } from './providers/uploader.service';
|
||||||
|
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||||
|
|
||||||
|
@Controller('uploader')
|
||||||
|
export class UploaderController {
|
||||||
|
constructor(private readonly uploaderService: UploaderService) {}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Upload a file (admin)' })
|
||||||
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
@ApiBody({ type: UploadSingleFileDto })
|
||||||
|
@Post('single-file')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
uploadFile(@UploadedFile() file: File) {
|
||||||
|
return this.uploaderService.uploadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Uploads multiple files' })
|
||||||
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@UseInterceptors(FilesInterceptor('files'))
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiBody({ type: UploadMultipleFileDto })
|
||||||
|
@Post('multi-file')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
uploadFiles(@UploadedFiles() files: File[]) {
|
||||||
|
return this.uploaderService.uploadMultiple(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { S3Service } from './providers/s3.service';
|
||||||
|
import { UploaderService } from './providers/uploader.service';
|
||||||
|
import { UploaderController } from './uploader.controller';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [UploaderController],
|
||||||
|
providers: [UploaderService, S3Service],
|
||||||
|
exports: [UploaderService, S3Service],
|
||||||
|
imports: [JwtModule],
|
||||||
|
})
|
||||||
|
export class UploaderModule {}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, Min, IsEnum, IsIn } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { EducationLevel, User } from '../entities/user.entity';
|
||||||
|
|
||||||
|
// Define the valid sort directions
|
||||||
|
const sortOrderOptions = ['asc', 'desc'] as const;
|
||||||
|
type SortOrder = (typeof sortOrderOptions)[number];
|
||||||
|
|
||||||
|
export class FindUsersDto {
|
||||||
|
/**
|
||||||
|
* The page number to retrieve.
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The page number to retrieve.',
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
minimum: 1,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of items per page.
|
||||||
|
* @default 10
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The number of items per page.',
|
||||||
|
type: Number,
|
||||||
|
default: 10,
|
||||||
|
minimum: 1,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A general search term to filter by.
|
||||||
|
* Searches firstName, lastName, phone, and nationalCode.
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'A general search term (firstName, lastName, phone, nationalCode).',
|
||||||
|
type: String,
|
||||||
|
example: 'John Doe',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by a specific education level.
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Filter by a specific education level.',
|
||||||
|
enum: EducationLevel, // Use the actual enum
|
||||||
|
example: EducationLevel.bachelor,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(EducationLevel)
|
||||||
|
education?: EducationLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The field to sort the results by.
|
||||||
|
* @default "createdAt"
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The field to sort the results by.',
|
||||||
|
type: String,
|
||||||
|
default: 'createdAt',
|
||||||
|
// You can provide a list of valid keys for better documentation
|
||||||
|
// You must ensure 'keyof User' is correctly exported/available here
|
||||||
|
example: 'lastName',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
orderBy?: keyof User = 'createdAt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The direction to sort the results.
|
||||||
|
* @default "desc"
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'The direction to sort the results.',
|
||||||
|
enum: sortOrderOptions, // Use the constant array
|
||||||
|
default: 'desc',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(sortOrderOptions)
|
||||||
|
order?: SortOrder = 'desc';
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { IsString, IsNotEmpty, IsBoolean, IsNumber, IsEnum, MinLength } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { EducationLevel } from '../entities/user.entity';
|
||||||
|
|
||||||
|
export class UpdateUserDto {
|
||||||
|
@ApiProperty({ example: ' ', description: "User's first name" })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: ' ', description: "User's last name" })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: ' ', description: "User's father's name" })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
fatherName: string;
|
||||||
|
|
||||||
|
// --- Personal Details ---
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: true, description: 'Gender: true for male, false for female' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsBoolean()
|
||||||
|
isMale: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '1234567890', description: 'Unique national code' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(10)
|
||||||
|
nationalCode: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '1001', description: 'Employee personal code' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
personalCode: string;
|
||||||
|
|
||||||
|
// --- Employment Details ---
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'قراردادی', description: 'Type of hire (e.g., permanent, temporary)' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
hireType: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'اراک', description: 'Primary work location' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
workLocation: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 5, description: 'Years of work experience' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
workExperienceYear: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: EducationLevel,
|
||||||
|
example: EducationLevel.master,
|
||||||
|
description: 'Highest level of education achieved',
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(EducationLevel)
|
||||||
|
education: EducationLevel;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Entity, Enum, PrimaryKey, Property, OptionalProps, OneToMany, Collection } from '@mikro-orm/core';
|
||||||
|
import { Submition } from '../../submition/entities/submition.entity';
|
||||||
|
|
||||||
|
export enum EducationLevel {
|
||||||
|
diploma = 'دیپلم',
|
||||||
|
associate = 'کاردانی',
|
||||||
|
bachelor = 'کارشناسی',
|
||||||
|
master = 'کارشناسی ارشد',
|
||||||
|
phd = 'دکتری',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserStatus {
|
||||||
|
finished = 'finished',
|
||||||
|
pending = 'pending',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ tableName: 'users' })
|
||||||
|
export class User {
|
||||||
|
[OptionalProps]?: 'id' | 'createdAt' | 'updatedAt';
|
||||||
|
|
||||||
|
@OneToMany(() => Submition, submition => submition.user)
|
||||||
|
submissions = new Collection<Submition>(this);
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
firstName?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
lastName?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
fatherName?: string;
|
||||||
|
|
||||||
|
@Property({ unique: true })
|
||||||
|
phone!: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
isMale?: boolean;
|
||||||
|
|
||||||
|
@Property({ unique: true, nullable: true })
|
||||||
|
nationalCode?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
personalCode?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
hireType?: string;
|
||||||
|
|
||||||
|
@Property({ nullable: true })
|
||||||
|
workLocation?: string;
|
||||||
|
|
||||||
|
@Property({ default: 0, nullable: true })
|
||||||
|
workExperienceYear?: number;
|
||||||
|
|
||||||
|
@Enum({ items: () => EducationLevel, nullable: true })
|
||||||
|
education?: EducationLevel;
|
||||||
|
|
||||||
|
@Enum({ items: () => UserStatus, nullable: true, default: UserStatus.pending })
|
||||||
|
status?: UserStatus;
|
||||||
|
|
||||||
|
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||||
|
createdAt: Date = new Date();
|
||||||
|
|
||||||
|
@Property({ onUpdate: () => new Date(), defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||||
|
updatedAt: Date = new Date();
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Controller, Get, Patch, Body, Req, UseGuards, Query, ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||||
|
import { AuthGuard, type AuthRequest } from 'src/common/guards/auth.guard';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
import { FindUsersDto } from './dto/find-user.dto';
|
||||||
|
import { AdminAuthGuard } from 'src/common/guards/auth.admin.guard';
|
||||||
|
|
||||||
|
@ApiTags('User')
|
||||||
|
@Controller('user')
|
||||||
|
export class UserController {
|
||||||
|
constructor(private readonly userService: UserService) {}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Get the current authenticated user profile' })
|
||||||
|
@Get('/')
|
||||||
|
async getUser(@Req() req: AuthRequest) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const user = await this.userService.findById(+userId);
|
||||||
|
return {
|
||||||
|
message: `GET request: Retrieving profile for authenticated user `,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update User Specification (PATCH /user/spec)
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Update the user specification' })
|
||||||
|
@ApiBody({ type: UpdateUserDto })
|
||||||
|
@Patch('/')
|
||||||
|
async updateUserSpec(@Req() req: AuthRequest, @Body() dto: UpdateUserDto) {
|
||||||
|
const userId = req.user.sub;
|
||||||
|
const user = await this.userService.updateUser(+userId, dto);
|
||||||
|
return {
|
||||||
|
message: `PATCH request: Updating specification for user ${userId} `,
|
||||||
|
userId,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: 'Users : Admin Route' })
|
||||||
|
@Get('/admin/users')
|
||||||
|
async findAll(
|
||||||
|
// Use ValidationPipe to validate and transform the DTO
|
||||||
|
@Query(new ValidationPipe({ transform: true, whitelist: true }))
|
||||||
|
query: FindUsersDto,
|
||||||
|
) {
|
||||||
|
return this.userService.findAll(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
import { UserController } from './user.controller';
|
||||||
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [UserService],
|
||||||
|
controllers: [UserController],
|
||||||
|
imports: [MikroOrmModule.forFeature([User]), JwtModule],
|
||||||
|
exports: [UserService],
|
||||||
|
})
|
||||||
|
export class UserModule {}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@mikro-orm/nestjs';
|
||||||
|
import { EntityRepository, FilterQuery } from '@mikro-orm/core';
|
||||||
|
import { User, UserStatus } from './entities/user.entity';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
import { FindUsersDto } from './dto/find-user.dto';
|
||||||
|
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private readonly userRepository: EntityRepository<User>,
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findOrCreateByPhone(phone: string): Promise<User> {
|
||||||
|
let user = await this.userRepository.findOne({ phone });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = this.userRepository.create({
|
||||||
|
phone,
|
||||||
|
});
|
||||||
|
await this.em.persistAndFlush(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser(userId: number, dto: UpdateUserDto): Promise<User> {
|
||||||
|
const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
|
||||||
|
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.em.assign(user, { ...dto, status: UserStatus.finished });
|
||||||
|
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByPhone(phone: string): Promise<User | null> {
|
||||||
|
return this.userRepository.findOne({ phone });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: number): Promise<User | null> {
|
||||||
|
return this.userRepository.findOne({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(phone: string): Promise<User> {
|
||||||
|
const user = this.userRepository.create({ phone });
|
||||||
|
await this.em.persistAndFlush(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||||
|
const { page = 1, limit = 10, search, education, orderBy = 'createdAt', order = 'desc' } = dto;
|
||||||
|
|
||||||
|
// 1. Calculate pagination
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// 2. Build the 'where' filter query
|
||||||
|
const where: FilterQuery<User> = {};
|
||||||
|
|
||||||
|
if (education) {
|
||||||
|
where.education = education;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Add 'search' logic (case-insensitive)
|
||||||
|
if (search) {
|
||||||
|
const searchPattern = `%${search}%`;
|
||||||
|
// $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
|
||||||
|
where.$or = [
|
||||||
|
{ firstName: { $ilike: searchPattern } },
|
||||||
|
{ lastName: { $ilike: searchPattern } },
|
||||||
|
{ phone: { $ilike: searchPattern } },
|
||||||
|
{ nationalCode: { $ilike: searchPattern } },
|
||||||
|
{ personalCode: { $ilike: searchPattern } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Execute the query using findAndCount
|
||||||
|
const [users, total] = await this.userRepository.findAndCount(where, {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Calculate total pages
|
||||||
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
|
// 7. Return the paginated result
|
||||||
|
return {
|
||||||
|
data: users,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+150
@@ -0,0 +1,150 @@
|
|||||||
|
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
||||||
|
import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(CacheService.name);
|
||||||
|
async onModuleInit() {
|
||||||
|
this.logger.log('Pinging Redis to check connection...');
|
||||||
|
try {
|
||||||
|
// 3. Try to set and get a test key
|
||||||
|
await this.cacheManager.set('ping', 'pong', 10); // 10-second TTL
|
||||||
|
const result = await this.cacheManager.get('ping');
|
||||||
|
|
||||||
|
if (result === 'pong') {
|
||||||
|
this.logger.log('✅ Redis connection successful (set/get test passed).');
|
||||||
|
} else {
|
||||||
|
this.logger.error(`❌ Redis connection test failed. Expected 'pong', got '${result}'.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Clean up the test key
|
||||||
|
await this.cacheManager.del('ping');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('❌ Failed to connect to Redis during ping test:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = 160) {
|
||||||
|
await this.cacheManager.set(key, value, ttl * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string) {
|
||||||
|
await this.cacheManager.del(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTtl(key: string) {
|
||||||
|
return await this.cacheManager.ttl(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// // src/modules/cache/cache.service.ts
|
||||||
|
// import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
||||||
|
// import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
|
|
||||||
|
// @Injectable()
|
||||||
|
// export class CacheService implements OnModuleInit {
|
||||||
|
// private readonly logger = new Logger(CacheService.name);
|
||||||
|
|
||||||
|
// constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * On-init lifecycle hook to test the cache connection.
|
||||||
|
// */
|
||||||
|
// async onModuleInit() {
|
||||||
|
// this.logger.log('Pinging Redis to check connection...');
|
||||||
|
// const testKey = 'ping';
|
||||||
|
// const testValue = 'pong';
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // 1. Set test key with a 10-second TTL
|
||||||
|
// // Note: We use the wrapper to ensure consistent TTL logic (seconds -> ms)
|
||||||
|
// await this.set(testKey, testValue, 10);
|
||||||
|
|
||||||
|
// // 2. Get test key
|
||||||
|
// const result = await this.get(testKey);
|
||||||
|
|
||||||
|
// if (result === testValue) {
|
||||||
|
// this.logger.log('✅ Redis connection successful (set/get test passed).');
|
||||||
|
// } else {
|
||||||
|
// this.logger.error(
|
||||||
|
// `❌ Redis connection test failed. Expected '${testValue}', got '${result}'.`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 3. Clean up the test key
|
||||||
|
// await this.del(testKey);
|
||||||
|
// } catch (error) {
|
||||||
|
// this.logger.error(
|
||||||
|
// '❌ Failed to connect to Redis during ping test:',
|
||||||
|
// error.message,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Gets a value from the cache.
|
||||||
|
// * @param key The cache key.
|
||||||
|
// * @returns The cached value or undefined.
|
||||||
|
// */
|
||||||
|
// async get<T>(key: string): Promise<T | undefined> {
|
||||||
|
// try {
|
||||||
|
// return await this.cacheManager.get<T>(key);
|
||||||
|
// } catch (error) {
|
||||||
|
// this.logger.error(`Error getting from cache (key: ${key}):`, error.message);
|
||||||
|
// return undefined; // Fail gracefully
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Sets a value in the cache.
|
||||||
|
// * @param key The cache key.
|
||||||
|
// * @param value The value to store (can be any serializable type).
|
||||||
|
// * @param ttlInSeconds The time-to-live in **seconds**.
|
||||||
|
// */
|
||||||
|
// async set(key: string, value: unknown, ttlInSeconds: number = 160): Promise<void> {
|
||||||
|
// try {
|
||||||
|
// // Convert seconds to milliseconds for cache-manager
|
||||||
|
// await this.cacheManager.set(key, value, ttlInSeconds * 1000);
|
||||||
|
// } catch (error) {
|
||||||
|
// this.logger.error(`Error setting cache (key: ${key}):`, error.message);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Deletes a value from the cache.
|
||||||
|
// * @param key The cache key to delete.
|
||||||
|
// */
|
||||||
|
// async del(key: string): Promise<void> {
|
||||||
|
// try {
|
||||||
|
// await this.cacheManager.del(key);
|
||||||
|
// } catch (error)
|
||||||
|
// {
|
||||||
|
// this.logger.error(`Error deleting from cache (key: ${key}):`, error.message);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Gets the remaining TTL for a key in seconds.
|
||||||
|
// * Note: This implementation is specific to redis-store.
|
||||||
|
// * @param key The cache key.
|
||||||
|
// * @returns The remaining TTL in seconds, or -1 (no expiry) / -2 (key doesn't exist).
|
||||||
|
// */
|
||||||
|
// async getTtl(key: string): Promise<number | undefined> {
|
||||||
|
// try {
|
||||||
|
// // The `ttl` method from cache-manager-redis-store returns TTL in seconds
|
||||||
|
// return await this.cacheManager.ttl(key);
|
||||||
|
// } catch (error) {
|
||||||
|
// this.logger.error(`Error getting TTL (key: ${key}):`, error.message);
|
||||||
|
// return undefined;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
// import { HttpService } from '@nestjs/axios';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
// import { lastValueFrom } from 'rxjs';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface IParameterArray {
|
||||||
|
name: 'otp';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISmsResponse {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISmsVerifyBody {
|
||||||
|
Parameters: IParameterArray[];
|
||||||
|
Mobile: string;
|
||||||
|
TemplateId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SmsService {
|
||||||
|
private readonly baseURL: string;
|
||||||
|
private readonly OTP: string;
|
||||||
|
private readonly apiKey: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
// private readonly httpService: HttpService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.baseURL = this.configService.getOrThrow<string>('SMS_BASE_URL');
|
||||||
|
this.OTP = this.configService.getOrThrow<string>('SMS_PATTERN_OTP');
|
||||||
|
this.apiKey = this.configService.getOrThrow<string>('SMS_API_KEY');
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendOtp(mobile: string, otp: string) {
|
||||||
|
const smsData: ISmsVerifyBody = {
|
||||||
|
Parameters: [{ name: 'otp', value: otp }],
|
||||||
|
Mobile: mobile,
|
||||||
|
TemplateId: this.OTP,
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${this.baseURL}/send/verify`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post<ISmsResponse>(url, smsData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-KEY': this.apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (data.status !== 1) {
|
||||||
|
throw new HttpException('The SMS was not sent. Please try again later.', HttpStatus.BAD_GATEWAY);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SMS sending failed:', JSON.stringify(error));
|
||||||
|
throw new HttpException('The SMS was not sent. Please try again later.', HttpStatus.BAD_GATEWAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CacheService } from './cache.service';
|
||||||
|
import { SmsService } from './sms.service';
|
||||||
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
import { cacheConfig } from '../../config/cache.config';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [CacheModule.registerAsync(cacheConfig())],
|
||||||
|
controllers: [],
|
||||||
|
providers: [CacheService, SmsService],
|
||||||
|
exports: [CacheService, SmsService],
|
||||||
|
})
|
||||||
|
export class UtilsModule {}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user