forked from Yara724/api
Merge pull request 'ENV edit, Joi validation' (#88) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#88
This commit is contained in:
8272
package-lock.json
generated
8272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yara724",
|
"name": "yara724",
|
||||||
"version": "0.0.1",
|
"version": "2.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -16,55 +16,61 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=10.0.0",
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fraybabak/kavenegar_nest": "^1.0.5",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/axios": "^3.1.3",
|
"@nestjs/common": "^11.0.17",
|
||||||
"@nestjs/common": "^10.4.15",
|
|
||||||
"@nestjs/config": "^4.0.4",
|
"@nestjs/config": "^4.0.4",
|
||||||
"@nestjs/core": "^10.4.15",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^11.0.2",
|
||||||
"@nestjs/mongoose": "^10.1.0",
|
"@nestjs/mongoose": "^11.0.4",
|
||||||
"@nestjs/platform-express": "^10.4.15",
|
"@nestjs/platform-express": "^11.1.11",
|
||||||
"@nestjs/schedule": "^4.1.2",
|
"@nestjs/serve-static": "^5.0.5",
|
||||||
"@nestjs/serve-static": "^4.0.2",
|
"@nestjs/swagger": "^11.4.4",
|
||||||
"@nestjs/swagger": "^7.4.2",
|
"axios": "^1.16.1",
|
||||||
"axios": "^1.9.0",
|
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.15.1",
|
||||||
"dotenv": "^16.4.7",
|
"express": "^5.2.1",
|
||||||
"express": "^4.22.1",
|
|
||||||
"express-basic-auth": "^1.2.1",
|
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"form-data": "^4.0.2",
|
"joi": "^18.2.1",
|
||||||
"jalali-moment": "^3.3.11",
|
|
||||||
"mongoose": "^8.9.2",
|
"mongoose": "^8.9.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"svg-captcha": "^1.4.0",
|
"svg-captcha": "^1.4.0"
|
||||||
"uuid": "^11.0.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^11.0.14",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
"@nestjs/schematics": "^10.2.3",
|
"@eslint/js": "^9.18.0",
|
||||||
"@nestjs/testing": "^10.4.15",
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@swc/cli": "^0.8.1",
|
||||||
|
"@swc/core": "^1.10.8",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/multer": "^1.4.12",
|
"@types/multer": "^2.1.0",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
"@types/supertest": "^6.0.2",
|
||||||
"@typescript-eslint/parser": "^8.18.1",
|
"eslint": "^9.18.0",
|
||||||
"eslint": "^9.17.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-plugin-prettier": "^5.2.3",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"globals": "^15.14.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
@@ -81,9 +87,12 @@
|
|||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node"
|
||||||
"moduleNameMapper": {
|
},
|
||||||
"^src/(.*)$": "<rootDir>/$1"
|
"pnpm": {
|
||||||
}
|
"onlyBuiltDependencies": [
|
||||||
|
"@nestjs/core",
|
||||||
|
"@swc/core"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
OnModuleInit,
|
OnModuleInit,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import axios, { AxiosRequestConfig } from "axios";
|
import { AxiosRequestConfig } from "axios"; // TODO: Change all axios usages to HttpModule
|
||||||
import * as FormData from "form-data";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiService implements OnModuleInit {
|
export class AiService implements OnModuleInit {
|
||||||
@@ -54,10 +53,10 @@ export class AiService implements OnModuleInit {
|
|||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
try {
|
try {
|
||||||
const res = await this.login();
|
const res = await this.login();
|
||||||
if (res?.accessToken) {
|
// if (res?.accessToken) {
|
||||||
this.accessToken = res.accessToken;
|
// this.accessToken = res.accessToken;
|
||||||
await this.getApiKey();
|
// await this.getApiKey();
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Don't prevent app startup if AI service is temporarily unavailable
|
// Don't prevent app startup if AI service is temporarily unavailable
|
||||||
// The service will attempt to re-authenticate when aiRequestImage is called
|
// The service will attempt to re-authenticate when aiRequestImage is called
|
||||||
@@ -68,14 +67,14 @@ export class AiService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async login() {
|
private async login() {
|
||||||
const loginResponse = await axios.request(this.loginOptions);
|
// const loginResponse = await axios.request(this.loginOptions);
|
||||||
return loginResponse.data;
|
// return loginResponse.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getApiKey() {
|
private async getApiKey() {
|
||||||
const profileResponse = await axios.request(this.profileOptions);
|
// const profileResponse = await axios.request(this.profileOptions);
|
||||||
this.apiKey = profileResponse.data.apiKey.key;
|
// this.apiKey = profileResponse.data.apiKey.key;
|
||||||
return this.apiKey;
|
// return this.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async aiRequestImage(file: {
|
public async aiRequestImage(file: {
|
||||||
@@ -86,15 +85,15 @@ export class AiService implements OnModuleInit {
|
|||||||
if (!this.accessToken || !this.apiKey) {
|
if (!this.accessToken || !this.apiKey) {
|
||||||
try {
|
try {
|
||||||
const res = await this.login();
|
const res = await this.login();
|
||||||
if (res?.accessToken) {
|
// if (res?.accessToken) {
|
||||||
this.accessToken = res.accessToken;
|
// this.accessToken = res.accessToken;
|
||||||
await this.getApiKey();
|
// await this.getApiKey();
|
||||||
} else {
|
// } else {
|
||||||
throw new HttpException(
|
// throw new HttpException(
|
||||||
"AI Service authentication failed",
|
// "AI Service authentication failed",
|
||||||
HttpStatus.UNAUTHORIZED,
|
// HttpStatus.UNAUTHORIZED,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
"AI Service authentication failed",
|
"AI Service authentication failed",
|
||||||
@@ -116,61 +115,61 @@ export class AiService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = new FormData();
|
// const form = new FormData();
|
||||||
const fileStream = createReadStream(filePath);
|
const fileStream = createReadStream(filePath);
|
||||||
|
|
||||||
// Append file with filename if available
|
// Append file with filename if available
|
||||||
if (file.fileName) {
|
if (file.fileName) {
|
||||||
form.append("images", fileStream, file.fileName);
|
// form.append("images", fileStream, file.fileName);
|
||||||
} else {
|
} else {
|
||||||
// Extract filename from path if not provided
|
// Extract filename from path if not provided
|
||||||
const pathParts = filePath.split("/");
|
const pathParts = filePath.split("/");
|
||||||
const extractedFileName = pathParts[pathParts.length - 1];
|
const extractedFileName = pathParts[pathParts.length - 1];
|
||||||
form.append("images", fileStream, extractedFileName);
|
// form.append("images", fileStream, extractedFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestHeaders = {
|
const requestHeaders = {
|
||||||
...this.imageProcessOptions.headers,
|
...this.imageProcessOptions.headers,
|
||||||
...form.getHeaders(),
|
// ...form.getHeaders(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
|
|
||||||
const response = await axios.request({
|
// const response = await axios.request({
|
||||||
...this.imageProcessOptions,
|
// ...this.imageProcessOptions,
|
||||||
headers: requestHeaders,
|
// headers: requestHeaders,
|
||||||
data: form,
|
// // data: form,
|
||||||
maxContentLength: Infinity,
|
// maxContentLength: Infinity,
|
||||||
maxBodyLength: Infinity,
|
// maxBodyLength: Infinity,
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Validate response structure
|
// Validate response structure
|
||||||
if (!response.data) {
|
// if (!response.data) {
|
||||||
throw new HttpException(
|
// throw new HttpException(
|
||||||
"AI Service returned empty response",
|
// "AI Service returned empty response",
|
||||||
HttpStatus.BAD_GATEWAY,
|
// HttpStatus.BAD_GATEWAY,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check for error in response first (AI service returns 201 with error in body)
|
// // Check for error in response first (AI service returns 201 with error in body)
|
||||||
if (response.data.error) {
|
// if (response.data.error) {
|
||||||
throw new HttpException(
|
// throw new HttpException(
|
||||||
`AI Service error: ${response.data.error}`,
|
// `AI Service error: ${response.data.error}`,
|
||||||
HttpStatus.BAD_GATEWAY,
|
// HttpStatus.BAD_GATEWAY,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check for processed image (downloadLink)
|
// // Check for processed image (downloadLink)
|
||||||
if (!response.data.downloadLink) {
|
// if (!response.data.downloadLink) {
|
||||||
throw new HttpException(
|
// throw new HttpException(
|
||||||
"AI Service did not return processed image (downloadLink missing)",
|
// "AI Service did not return processed image (downloadLink missing)",
|
||||||
HttpStatus.BAD_GATEWAY,
|
// HttpStatus.BAD_GATEWAY,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
return response.data;
|
// return response.data;
|
||||||
} catch (er) {
|
} catch (er) {
|
||||||
// Determine error source
|
// Determine error source
|
||||||
let errorSource = "UNKNOWN";
|
let errorSource = "UNKNOWN";
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { APP_INTERCEPTOR } from "@nestjs/core";
|
import { APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module, ValidationPipe } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||||
import { UnicodeDigitsNormalizeInterceptor } from "./common/interceptors/unicode-digits-normalize.interceptor";
|
import { UnicodeDigitsNormalizeInterceptor } from "./common/interceptors/unicode-digits-normalize.interceptor";
|
||||||
import { MongooseModule } from "@nestjs/mongoose";
|
import { MongooseModule } from "@nestjs/mongoose";
|
||||||
import { ScheduleModule } from "@nestjs/schedule";
|
|
||||||
import { ServeStaticModule } from "@nestjs/serve-static";
|
import { ServeStaticModule } from "@nestjs/serve-static";
|
||||||
import * as dotenv from "dotenv";
|
|
||||||
import { AiModule } from "./ai/ai.module";
|
import { AiModule } from "./ai/ai.module";
|
||||||
import { AuthModule } from "./auth/auth.module";
|
import { AuthModule } from "./auth/auth.module";
|
||||||
import { ClaimRequestManagementModule } from "./claim-request-management/claim-request-management.module";
|
import { ClaimRequestManagementModule } from "./claim-request-management/claim-request-management.module";
|
||||||
@@ -25,35 +23,77 @@ import { UsersModule } from "./users/users.module";
|
|||||||
import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin";
|
import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin";
|
||||||
import { CronModule } from "./utils/cron/cron.module";
|
import { CronModule } from "./utils/cron/cron.module";
|
||||||
import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module";
|
import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module";
|
||||||
|
import * as Joi from "joi";
|
||||||
dotenv.config();
|
|
||||||
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({
|
||||||
ScheduleModule.forRoot(),
|
isGlobal: true,
|
||||||
|
validationSchema: Joi.object({
|
||||||
|
NODE_ENV: Joi.string()
|
||||||
|
.valid("development", "production")
|
||||||
|
.default("development"),
|
||||||
|
PORT: Joi.number().port().default(9001),
|
||||||
|
URL: Joi.string().uri().required(),
|
||||||
|
BASE_URL_DEV: Joi.string()
|
||||||
|
.uri()
|
||||||
|
.when("NODE_ENV", {
|
||||||
|
is: Joi.string().valid("development"),
|
||||||
|
then: Joi.required(),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
SWAGGER_USER_DEV: Joi.string().when("NODE_ENV", {
|
||||||
|
is: Joi.string().valid("development"),
|
||||||
|
then: Joi.required(),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
SWAGGER_PASSWORD_DEV: Joi.string().when("NODE_ENV", {
|
||||||
|
is: Joi.string().valid("development"),
|
||||||
|
then: Joi.required(),
|
||||||
|
otherwise: Joi.optional(),
|
||||||
|
}),
|
||||||
|
MONGO_HOST: Joi.string().required(),
|
||||||
|
MONGO_PORT: Joi.number().port().required(),
|
||||||
|
MONGO_USER: Joi.string().required(),
|
||||||
|
MONGO_PASS: Joi.string().required(),
|
||||||
|
MONGO_DB_NAME: Joi.string().required(),
|
||||||
|
JWT_SECRET: Joi.string().required(),
|
||||||
|
SANHUB_BASE_URL: Joi.string().uri(),
|
||||||
|
SANHUB_URL_LOGIN: Joi.string().uri(),
|
||||||
|
SANHUB_USERNAME: Joi.string(),
|
||||||
|
SANHUB_PASSWORD: Joi.string(),
|
||||||
|
AI_URL: Joi.string().uri(),
|
||||||
|
AI_URL_V2: Joi.string().uri(),
|
||||||
|
AI_USERNAME: Joi.string(),
|
||||||
|
AI_PASSWORD: Joi.string(),
|
||||||
|
SMS_PROVIDER: Joi.string()
|
||||||
|
.valid("kavenegar", "parsian")
|
||||||
|
.default("kavenegar"),
|
||||||
|
SMS_API_KEY: Joi.string(),
|
||||||
|
AUTH_SMS_TEMPLATE: Joi.string(),
|
||||||
|
EXP_OTP_TIME: Joi.number(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
CronModule,
|
CronModule,
|
||||||
ServeStaticModule.forRoot({
|
ServeStaticModule.forRoot({
|
||||||
rootPath: join(__dirname, "..", "files"),
|
rootPath: join(__dirname, "..", "files"),
|
||||||
serveRoot: "/files",
|
serveRoot: "/files",
|
||||||
}),
|
}),
|
||||||
MongooseModule.forRoot(
|
MongooseModule.forRootAsync({
|
||||||
`mongodb://${process.env.MONGO_URL}:${process.env.MONGO_PORT}/`,
|
inject: [ConfigService],
|
||||||
{
|
useFactory: (configService: ConfigService) => {
|
||||||
dbName: "yara724",
|
return {
|
||||||
autoIndex: true,
|
uri: `mongodb://${configService.get<string>("MONGO_USER")}:${configService.get<string>("MONGO_PASS")}@${configService.get<string>("MONGO_HOST")}:${configService.get<string>("MONGO_PORT")}/${configService.get<string>("MONGO_DB_NAME")}?authSource=admin&${configService.get<string>("MONGO_OPTIONS")}`,
|
||||||
user: process.env.MONGO_USER,
|
tls: true,
|
||||||
pass: process.env.MONGO_PASS,
|
tlsAllowInvalidCertificates: true,
|
||||||
authMechanism: "SCRAM-SHA-256",
|
autoIndex: configService.get<string>("NODE_ENV") !== "production",
|
||||||
tls: true,
|
connectionFactory: (connection) => {
|
||||||
tlsAllowInvalidCertificates: true,
|
applyIranFaTimestampPlugin(connection);
|
||||||
connectionFactory: (connection) => {
|
return connection;
|
||||||
applyIranFaTimestampPlugin(connection);
|
},
|
||||||
return connection;
|
};
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
}),
|
||||||
UsersModule,
|
UsersModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ export class ActorAuthService {
|
|||||||
res = await this.fieldExpertDbService.findOne({
|
res = await this.fieldExpertDbService.findOne({
|
||||||
_id: new Types.ObjectId(userId),
|
_id: new Types.ObjectId(userId),
|
||||||
});
|
});
|
||||||
else
|
else res = await this.fieldExpertDbService.findOne({ email: username });
|
||||||
res = await this.fieldExpertDbService.findOne({ email: username });
|
|
||||||
break;
|
break;
|
||||||
case RoleEnum.REGISTRAR:
|
case RoleEnum.REGISTRAR:
|
||||||
if (username == null && userId)
|
if (username == null && userId)
|
||||||
@@ -139,7 +138,7 @@ export class ActorAuthService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const access_token = this.jwtService.sign(payload, {
|
const access_token = this.jwtService.sign(payload, {
|
||||||
secret: `${process.env.SECRET}`,
|
secret: `${process.env.JWT_SECRET}`,
|
||||||
expiresIn: "1h",
|
expiresIn: "1h",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,9 +158,7 @@ export class ActorAuthService {
|
|||||||
throw new UnauthorizedException("user not assigned to this role");
|
throw new UnauthorizedException("user not assigned to this role");
|
||||||
}
|
}
|
||||||
if (!(await this.hashService.compare(pass, user.password))) {
|
if (!(await this.hashService.compare(pass, user.password))) {
|
||||||
throw new UnauthorizedException(
|
throw new UnauthorizedException("password is incorrect or access Denied");
|
||||||
"password is incorrect or access Denied",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -225,7 +222,7 @@ export class ActorAuthService {
|
|||||||
firstName: body.firstName,
|
firstName: body.firstName,
|
||||||
lastName: body.lastName,
|
lastName: body.lastName,
|
||||||
phone: body.phone,
|
phone: body.phone,
|
||||||
mobile:body.mobile,
|
mobile: body.mobile,
|
||||||
city: body.city,
|
city: body.city,
|
||||||
state: body.state,
|
state: body.state,
|
||||||
address: body.address,
|
address: body.address,
|
||||||
@@ -445,13 +442,7 @@ export class ActorAuthService {
|
|||||||
"state",
|
"state",
|
||||||
"address",
|
"address",
|
||||||
],
|
],
|
||||||
field_expert: [
|
field_expert: ["firstName", "lastName", "email", "phone", "mobile"],
|
||||||
"firstName",
|
|
||||||
"lastName",
|
|
||||||
"email",
|
|
||||||
"phone",
|
|
||||||
"mobile",
|
|
||||||
],
|
|
||||||
registrar: ["email"],
|
registrar: ["email"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -477,7 +468,11 @@ export class ActorAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch user detail (document or plain object)
|
// fetch user detail (document or plain object)
|
||||||
const document = await this.dynamicDbController(role, currentUser.role === "company" ? currentUser.username : null, userId);
|
const document = await this.dynamicDbController(
|
||||||
|
role,
|
||||||
|
currentUser.role === "company" ? currentUser.username : null,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
if (!document) throw new NotFoundException("Profile not found");
|
if (!document) throw new NotFoundException("Profile not found");
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class UserAuthService {
|
|||||||
role: "user",
|
role: "user",
|
||||||
};
|
};
|
||||||
const accToken = this.jwtService.sign(payload, {
|
const accToken = this.jwtService.sign(payload, {
|
||||||
secret: `${process.env.SECRET}`,
|
secret: `${process.env.JWT_SECRET}`,
|
||||||
});
|
});
|
||||||
await this.userDbService.findOneAndUpdate(
|
await this.userDbService.findOneAndUpdate(
|
||||||
{ username: user.username },
|
{ username: user.username },
|
||||||
@@ -162,10 +162,7 @@ export class UserAuthService {
|
|||||||
process.env.AUTH_SMS_TEMPLATE,
|
process.env.AUTH_SMS_TEMPLATE,
|
||||||
);
|
);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
throw new HttpException(
|
throw new HttpException("auth sms send failed", HttpStatus.BAD_GATEWAY);
|
||||||
"auth sms send failed",
|
|
||||||
HttpStatus.BAD_GATEWAY,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`,
|
`Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`,
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ import { CaptchaModule } from "src/captcha/captcha.module";
|
|||||||
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||||
]),
|
]),
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
signOptions: { expiresIn: "1h" },
|
signOptions: { expiresIn: "1h" }, // TODO: MAKE IT ENV
|
||||||
global: true,
|
global: true,
|
||||||
secret: `${process.env.SECRET}`,
|
secret: `${process.env.JWT_SECRET}`,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class LocalActorAuthGuard implements CanActivate {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await this.jwtService.verifyAsync(token, {
|
const payload = await this.jwtService.verifyAsync(token, {
|
||||||
secret: `${process.env.SECRET}`,
|
secret: `${process.env.JWT_SECRET}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class ClaimAccessGuard implements CanActivate {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await this.jwtService.verifyAsync(token, {
|
const payload = await this.jwtService.verifyAsync(token, {
|
||||||
secret: `${process.env.SECRET}`,
|
secret: `${process.env.JWT_SECRET}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Allow users to pass through (they will be checked by service methods)
|
// Allow users to pass through (they will be checked by service methods)
|
||||||
@@ -81,7 +81,10 @@ export class ClaimAccessGuard implements CanActivate {
|
|||||||
|
|
||||||
throw new UnauthorizedException("Invalid role");
|
throw new UnauthorizedException("Invalid role");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ForbiddenException || error instanceof UnauthorizedException) {
|
if (
|
||||||
|
error instanceof ForbiddenException ||
|
||||||
|
error instanceof UnauthorizedException
|
||||||
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
@@ -124,4 +127,3 @@ export class ClaimAccessGuard implements CanActivate {
|
|||||||
return type === "Bearer" ? token : undefined;
|
return type === "Bearer" ? token : undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ export class GlobalGuard implements CanActivate {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await this.jwtService.verifyAsync(token, {
|
const payload = await this.jwtService.verifyAsync(token, {
|
||||||
secret: `${process.env.SECRET}`,
|
secret: `${process.env.JWT_SECRET}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.role !== RoleEnum.USER && payload.role !== RoleEnum.FIELD_EXPERT) {
|
if (
|
||||||
|
payload.role !== RoleEnum.USER &&
|
||||||
|
payload.role !== RoleEnum.FIELD_EXPERT
|
||||||
|
) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class SettingsJwtGuard implements CanActivate {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await this.jwtService.verifyAsync(token, {
|
const payload = await this.jwtService.verifyAsync(token, {
|
||||||
secret: `${process.env.SECRET}`,
|
secret: `${process.env.JWT_SECRET}`,
|
||||||
});
|
});
|
||||||
(request as any).user = payload;
|
(request as any).user = payload;
|
||||||
(request as any).identity = payload;
|
(request as any).identity = payload;
|
||||||
|
|||||||
@@ -127,9 +127,7 @@ export class ClaimRequestManagementController {
|
|||||||
@ApiOperation({ deprecated: true })
|
@ApiOperation({ deprecated: true })
|
||||||
@Get("required-documents-status/:claimRequestID")
|
@Get("required-documents-status/:claimRequestID")
|
||||||
@ApiParam({ name: "claimRequestID" })
|
@ApiParam({ name: "claimRequestID" })
|
||||||
async getRequiredDocumentsStatus(
|
async getRequiredDocumentsStatus(@Param("claimRequestID") requestId: string) {
|
||||||
@Param("claimRequestID") requestId: string,
|
|
||||||
) {
|
|
||||||
return await this.claimRequestManagementService.getRequiredDocumentsStatus(
|
return await this.claimRequestManagementService.getRequiredDocumentsStatus(
|
||||||
requestId,
|
requestId,
|
||||||
);
|
);
|
||||||
@@ -293,10 +291,7 @@ export class ClaimRequestManagementController {
|
|||||||
@ApiOperation({ deprecated: true })
|
@ApiOperation({ deprecated: true })
|
||||||
@Get("request/:claimRequestId")
|
@Get("request/:claimRequestId")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
@ApiParam({ name: "claimRequestId" })
|
||||||
myRequests(
|
myRequests(@Param("claimRequestId") requestId: string, @CurrentUser() user) {
|
||||||
@Param("claimRequestId") requestId: string,
|
|
||||||
@CurrentUser() user,
|
|
||||||
) {
|
|
||||||
return this.claimRequestManagementService.requestDetails(requestId, user);
|
return this.claimRequestManagementService.requestDetails(requestId, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,12 +326,12 @@ export class ClaimRequestManagementController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@CurrentUser() user,
|
@CurrentUser() user,
|
||||||
) {
|
) {
|
||||||
return await this.claimRequestManagementService.submitUserReply(
|
// return await this.claimRequestManagementService.submitUserReply(
|
||||||
requestId,
|
// requestId,
|
||||||
body,
|
// body,
|
||||||
file,
|
// file,
|
||||||
user,
|
// user,
|
||||||
);
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
@ApiOperation({ deprecated: true })
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { ClaimRequestManagementService } from "./claim-request-management.servic
|
|||||||
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
||||||
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
||||||
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
|
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
|
||||||
import { ClaimCase, ClaimCaseSchema } from "./entites/schema/claim-cases.schema";
|
import {
|
||||||
|
ClaimCase,
|
||||||
|
ClaimCaseSchema,
|
||||||
|
} from "./entites/schema/claim-cases.schema";
|
||||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||||
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
||||||
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
||||||
@@ -47,9 +50,11 @@ import { ClientModule } from "src/client/client.module";
|
|||||||
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
||||||
import { JwtModule } from "@nestjs/jwt";
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
|
import { MediaPolicyModule } from "src/media-policy/media-policy.module";
|
||||||
|
import { HttpModule } from "@nestjs/axios";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
HttpModule,
|
||||||
PublicIdModule,
|
PublicIdModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
RequestManagementModule,
|
RequestManagementModule,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ import { readFile } from "node:fs/promises";
|
|||||||
import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger";
|
||||||
import { FileInterceptor } from "@nestjs/platform-express";
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
import { diskStorage } from "multer";
|
import { diskStorage } from "multer";
|
||||||
import { extname } from "path";
|
import { extname } from "node:path";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { GlobalGuard } from "src/auth/guards/global.guard";
|
import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Prop, Schema } from "@nestjs/mongoose";
|
import { Prop, Schema } from "@nestjs/mongoose";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
@Schema({ versionKey: false, _id: false })
|
@Schema({ versionKey: false, _id: false })
|
||||||
export class ImageRequiredModel {
|
export class ImageRequiredModel {
|
||||||
@@ -20,7 +20,7 @@ export class ImageRequiredModel {
|
|||||||
constructor(claimFile: any[]) {
|
constructor(claimFile: any[]) {
|
||||||
this.aroundTheCar.forEach((a) => {
|
this.aroundTheCar.forEach((a) => {
|
||||||
Object.assign(a, {
|
Object.assign(a, {
|
||||||
partId: uuidv4(),
|
partId: randomUUID(),
|
||||||
imageId: null,
|
imageId: null,
|
||||||
aiReport: {},
|
aiReport: {},
|
||||||
upload: false,
|
upload: false,
|
||||||
@@ -28,7 +28,7 @@ export class ImageRequiredModel {
|
|||||||
});
|
});
|
||||||
this.selectPartOfCar = claimFile.map((c, idx) =>
|
this.selectPartOfCar = claimFile.map((c, idx) =>
|
||||||
Object.assign(c, {
|
Object.assign(c, {
|
||||||
partId: uuidv4(),
|
partId: randomUUID(),
|
||||||
aiReport: {},
|
aiReport: {},
|
||||||
imageId: null,
|
imageId: null,
|
||||||
upload: false,
|
upload: false,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import * as jMoment from "jalali-moment";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an ISO date string or Date to Jalali (Persian) date and time.
|
* Converts an ISO date string or Date to Jalali (Persian) date and time.
|
||||||
* @param input - ISO date string (e.g. 2026-02-08T13:51:20.747+00:00) or Date
|
* @param input - ISO date string (e.g. 2026-02-08T13:51:20.747+00:00) or Date
|
||||||
* @returns Tuple [dateStr, timeStr] e.g. ['1404/11/24', '13:37']
|
* @returns Tuple [dateStr, timeStr] e.g. ['1404/11/24', '13:37']
|
||||||
*/
|
*/
|
||||||
export function toJalaliDateAndTime(
|
export function toJalaliDateAndTime(input: string | Date): [string, string] {
|
||||||
input: string | Date,
|
|
||||||
): [string, string] {
|
|
||||||
const d = new Date(input);
|
const d = new Date(input);
|
||||||
const dateStr = toJalaliDate(d);
|
const dateStr = toJalaliDate(d);
|
||||||
const timeStr = toJalaliTime(d);
|
const timeStr = toJalaliTime(d);
|
||||||
@@ -32,8 +28,7 @@ export function jalaliToGregorianDate(
|
|||||||
): string | null {
|
): string | null {
|
||||||
if (input === null || input === undefined) return null;
|
if (input === null || input === undefined) return null;
|
||||||
|
|
||||||
const raw =
|
const raw = typeof input === "number" ? String(input) : String(input).trim();
|
||||||
typeof input === "number" ? String(input) : String(input).trim();
|
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
|
|
||||||
let year = 0;
|
let year = 0;
|
||||||
@@ -58,20 +53,16 @@ export function jalaliToGregorianDate(
|
|||||||
|
|
||||||
if (!year || !month || !day) return null;
|
if (!year || !month || !day) return null;
|
||||||
|
|
||||||
// If the year already looks Gregorian (>= 1900), assume the caller already
|
// If the year already looks Gregorian (>= 1900), just normalise formatting.
|
||||||
// sent a Gregorian date and just normalise the formatting.
|
|
||||||
if (year >= 1900) {
|
if (year >= 1900) {
|
||||||
const mm = String(month).padStart(2, "0");
|
const mm = String(month).padStart(2, "0");
|
||||||
const dd = String(day).padStart(2, "0");
|
const dd = String(day).padStart(2, "0");
|
||||||
const m = jMoment(`${year}-${mm}-${dd}`, "YYYY-MM-DD");
|
const d = new Date(`${year}-${mm}-${dd}`);
|
||||||
return m.isValid() ? m.format("YYYY-MM-DD") : null;
|
if (isNaN(d.getTime())) return null;
|
||||||
|
return `${year}-${mm}-${dd}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mm = String(month).padStart(2, "0");
|
return jalaliPartsToGregorian(year, month, day);
|
||||||
const dd = String(day).padStart(2, "0");
|
|
||||||
const jm = jMoment(`${year}-${mm}-${dd}`, "jYYYY-jMM-jDD");
|
|
||||||
if (!jm.isValid()) return null;
|
|
||||||
return jm.format("YYYY-MM-DD");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,6 +86,92 @@ export function toJalaliTime(d: Date): string {
|
|||||||
return `${hours}:${minutes}`;
|
return `${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Jalali year/month/day parts to a Gregorian `YYYY-MM-DD` string
|
||||||
|
* using the algorithmic conversion (no external deps).
|
||||||
|
* Returns `null` if the resulting Gregorian date is invalid.
|
||||||
|
*/
|
||||||
|
function jalaliPartsToGregorian(
|
||||||
|
jYear: number,
|
||||||
|
jMonth: number,
|
||||||
|
jDay: number,
|
||||||
|
): string | null {
|
||||||
|
// Jalali to Julian Day Number, then to Gregorian
|
||||||
|
// Algorithm: https://www.fourmilab.ch/documents/calendar/
|
||||||
|
const jy = jYear - 979;
|
||||||
|
const jm = jMonth - 1;
|
||||||
|
const jd = jDay - 1;
|
||||||
|
|
||||||
|
let jDayNo =
|
||||||
|
365 * jy + Math.floor(jy / 4) - Math.floor(jy / 100) + Math.floor(jy / 400);
|
||||||
|
|
||||||
|
const jalaliMonthDays = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];
|
||||||
|
for (let i = 0; i < jm; i++) {
|
||||||
|
jDayNo += jalaliMonthDays[i];
|
||||||
|
}
|
||||||
|
jDayNo += jd;
|
||||||
|
|
||||||
|
// Convert to Gregorian day number (offset from 1970-01-01 in Julian days)
|
||||||
|
const gDayNo = jDayNo + 79;
|
||||||
|
|
||||||
|
let gy = 1979 + 400 * Math.floor(gDayNo / 146097);
|
||||||
|
let days = gDayNo % 146097;
|
||||||
|
|
||||||
|
let leap = true;
|
||||||
|
if (days >= 36525) {
|
||||||
|
days--;
|
||||||
|
gy += 100 * Math.floor(days / 36524);
|
||||||
|
days %= 36524;
|
||||||
|
if (days >= 365) days++;
|
||||||
|
else leap = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
gy += 4 * Math.floor(days / 1461);
|
||||||
|
days %= 1461;
|
||||||
|
|
||||||
|
if (days >= 366) {
|
||||||
|
leap = false;
|
||||||
|
days--;
|
||||||
|
gy += Math.floor(days / 365);
|
||||||
|
days %= 365;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gregorianMonthDays = [
|
||||||
|
31,
|
||||||
|
leap ? 29 : 28,
|
||||||
|
31,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
31,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
];
|
||||||
|
|
||||||
|
let gm = 0;
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
if (days < gregorianMonthDays[i]) {
|
||||||
|
gm = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
days -= gregorianMonthDays[i];
|
||||||
|
}
|
||||||
|
const gd = days + 1;
|
||||||
|
|
||||||
|
const mm = String(gm).padStart(2, "0");
|
||||||
|
const dd = String(gd).padStart(2, "0");
|
||||||
|
const result = `${gy}-${mm}-${dd}`;
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
const check = new Date(result);
|
||||||
|
if (isNaN(check.getTime())) return null;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const PERSIAN_TO_ENGLISH: Record<string, string> = {
|
const PERSIAN_TO_ENGLISH: Record<string, string> = {
|
||||||
"۰": "0",
|
"۰": "0",
|
||||||
"۱": "1",
|
"۱": "1",
|
||||||
|
|||||||
60
src/main.ts
60
src/main.ts
@@ -1,8 +1,8 @@
|
|||||||
import { NestFactory } from "@nestjs/core";
|
import { NestFactory } from "@nestjs/core";
|
||||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||||
import * as basicAuth from "express-basic-auth";
|
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||||
@@ -15,29 +15,64 @@ async function bootstrap() {
|
|||||||
allowedHeaders: "Content-Type, Authorization",
|
allowedHeaders: "Content-Type, Authorization",
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(
|
const configService = app.get(ConfigService);
|
||||||
["/docs"],
|
|
||||||
basicAuth({
|
const isDevelopment = configService.get<string>("NODE_ENV") === "development";
|
||||||
challenge: true,
|
|
||||||
users: {
|
if (isDevelopment) {
|
||||||
[process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD,
|
app.use("/docs", (req, res, next) => {
|
||||||
},
|
const authHeader = req.headers.authorization;
|
||||||
}),
|
|
||||||
);
|
const challenge = () => {
|
||||||
|
res.setHeader("WWW-Authenticate", 'Basic realm="Swagger"');
|
||||||
|
res.status(401).send("Unauthorized");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!authHeader?.startsWith("Basic ")) {
|
||||||
|
return challenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64 = authHeader.split(" ")[1];
|
||||||
|
const [username, ...rest] = Buffer.from(base64, "base64")
|
||||||
|
.toString("utf8")
|
||||||
|
.split(":");
|
||||||
|
const password = rest.join(":");
|
||||||
|
|
||||||
|
const expectedUser = configService.get<string>("SWAGGER_USER_DEV", "");
|
||||||
|
const expectedPassword = configService.get<string>(
|
||||||
|
"SWAGGER_PASSWORD_DEV",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (username !== expectedUser || password !== expectedPassword) {
|
||||||
|
return challenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch {
|
||||||
|
return challenge();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle("yara724-backend")
|
.setTitle("yara724-backend")
|
||||||
.setVersion("1.0.0")
|
.setVersion("1.0.0")
|
||||||
.addServer(process.env.BASE_URL + "/api")
|
.addServer(process.env.BASE_URL_DEV + "/api")
|
||||||
.addServer("http://192.168.20.170:9001")
|
.addServer("http://192.168.20.170:9001")
|
||||||
.addServer("http://localhost:9001")
|
.addServer("http://localhost:9001")
|
||||||
.addBearerAuth()
|
.addBearerAuth()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const docs = SwaggerModule.createDocument(app, config);
|
const docs = SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup("/docs", app, docs, {
|
|
||||||
|
SwaggerModule.setup("docs", app, docs, {
|
||||||
swaggerOptions: {
|
swaggerOptions: {
|
||||||
persistAuthorization: true,
|
persistAuthorization: true,
|
||||||
docExpansion: "none",
|
docExpansion: "none",
|
||||||
|
ui: isDevelopment ? true : false,
|
||||||
|
raw: isDevelopment ? true : false,
|
||||||
tagsSorter: (a: string, b: string) => {
|
tagsSorter: (a: string, b: string) => {
|
||||||
const priority: Record<string, number> = {
|
const priority: Record<string, number> = {
|
||||||
user: 0,
|
user: 0,
|
||||||
@@ -50,6 +85,7 @@ async function bootstrap() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.listen(process.env.PORT);
|
await app.listen(process.env.PORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,12 @@ import {
|
|||||||
ServiceUnavailableException,
|
ServiceUnavailableException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import axios from "axios";
|
|
||||||
import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service";
|
import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service";
|
||||||
import { SandHubModel } from "src/sand-hub/entity/schema/sand-hub.schema";
|
import { SandHubModel } from "src/sand-hub/entity/schema/sand-hub.schema";
|
||||||
import { SystemSettingsService } from "src/system-settings/system-settings.service";
|
import { SystemSettingsService } from "src/system-settings/system-settings.service";
|
||||||
import { SandHubDetailDto } from "./dto/sand-hub.dto";
|
import { SandHubDetailDto } from "./dto/sand-hub.dto";
|
||||||
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SandHubService {
|
export class SandHubService {
|
||||||
@@ -123,8 +123,7 @@ export class SandHubService {
|
|||||||
const u = (url || "").toLowerCase();
|
const u = (url || "").toLowerCase();
|
||||||
if (u.includes("personal-inquiry")) {
|
if (u.includes("personal-inquiry")) {
|
||||||
const p = payload as { nationalCode?: string } | undefined;
|
const p = payload as { nationalCode?: string } | undefined;
|
||||||
const nin =
|
const nin = typeof p?.nationalCode === "string" ? p.nationalCode : "-";
|
||||||
typeof p?.nationalCode === "string" ? p.nationalCode : "-";
|
|
||||||
return {
|
return {
|
||||||
message: "success",
|
message: "success",
|
||||||
data: {
|
data: {
|
||||||
@@ -182,12 +181,11 @@ export class SandHubService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.httpService.axiosRef.post(
|
const response = await firstValueFrom(
|
||||||
process.env.SANDHUB_URL_LOGIN,
|
this.httpService.post(process.env.SANHUB_URL_LOGIN, {
|
||||||
{
|
email: process.env.SANHUB_USERNAME,
|
||||||
email: process.env.SANDHUB_USERNAME,
|
password: process.env.SANHUB_PASSWORD,
|
||||||
password: process.env.SANDHUB_PASSWORD,
|
}),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const token = response.data.accessToken;
|
const token = response.data.accessToken;
|
||||||
@@ -237,16 +235,18 @@ export class SandHubService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.httpService.axiosRef.post(
|
const response = await firstValueFrom(
|
||||||
`${baseUrl}/user/login`,
|
this.httpService.post(
|
||||||
{ email, password },
|
`${baseUrl}/user/login`,
|
||||||
{
|
{ email, password },
|
||||||
headers: {
|
{
|
||||||
Accept: "*/*",
|
headers: {
|
||||||
"Content-Type": "application/json",
|
Accept: "*/*",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
timeout: 30000,
|
),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const token = response.data?.accessToken;
|
const token = response.data?.accessToken;
|
||||||
@@ -260,7 +260,10 @@ export class SandHubService {
|
|||||||
|
|
||||||
return this.tejaratAccessToken;
|
return this.tejaratAccessToken;
|
||||||
} catch (er) {
|
} catch (er) {
|
||||||
this.logger.error("Failed to login to Tejarat inquiry:", er?.message || er);
|
this.logger.error(
|
||||||
|
"Failed to login to Tejarat inquiry:",
|
||||||
|
er?.message || er,
|
||||||
|
);
|
||||||
this.tejaratAccessToken = null;
|
this.tejaratAccessToken = null;
|
||||||
this.tejaratTokenExpiry = null;
|
this.tejaratTokenExpiry = null;
|
||||||
throw new UnauthorizedException("Tejarat inquiry authentication failed");
|
throw new UnauthorizedException("Tejarat inquiry authentication failed");
|
||||||
@@ -278,14 +281,16 @@ export class SandHubService {
|
|||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
const token = await this.getTejaratAccessToken();
|
const token = await this.getTejaratAccessToken();
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(url, payload, {
|
const response = await firstValueFrom(
|
||||||
headers: {
|
this.httpService.post(url, payload, {
|
||||||
Authorization: `Bearer ${token}`,
|
headers: {
|
||||||
"Content-Type": "application/json",
|
Authorization: `Bearer ${token}`,
|
||||||
Accept: "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
Accept: "application/json",
|
||||||
timeout: 30000,
|
},
|
||||||
});
|
timeout: 30000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -361,14 +366,16 @@ export class SandHubService {
|
|||||||
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(url, payload, {
|
const response = await firstValueFrom(
|
||||||
headers: {
|
this.httpService.post(url, payload, {
|
||||||
Authorization: `Bearer ${token}`,
|
headers: {
|
||||||
"Content-Type": "application/json",
|
Authorization: `Bearer ${token}`,
|
||||||
Accept: "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
Accept: "application/json",
|
||||||
timeout: 50000,
|
},
|
||||||
});
|
timeout: 50000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -397,22 +404,32 @@ export class SandHubService {
|
|||||||
|
|
||||||
// Vehicle information
|
// Vehicle information
|
||||||
MapTypNam: newResponse.vehiclePersianName || newResponse.MapTypNam,
|
MapTypNam: newResponse.vehiclePersianName || newResponse.MapTypNam,
|
||||||
UsageField: newResponse.persianCarType || newResponse.UsageField || (newResponse.usgCod === "8" ? "شخصی" : "نامشخص"),
|
UsageField:
|
||||||
|
newResponse.persianCarType ||
|
||||||
|
newResponse.UsageField ||
|
||||||
|
(newResponse.usgCod === "8" ? "شخصی" : "نامشخص"),
|
||||||
MapUsageName: newResponse.MapUsageName || newResponse.MapUsageName,
|
MapUsageName: newResponse.MapUsageName || newResponse.MapUsageName,
|
||||||
|
|
||||||
// Financial coverage
|
// Financial coverage
|
||||||
FinancialCvrCptl: newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0",
|
FinancialCvrCptl:
|
||||||
|
newResponse.financeCoverage || newResponse.FinancialCvrCptl || "0",
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
IssueDate: newResponse.persianStartDate || newResponse.IssueDate,
|
IssueDate: newResponse.persianStartDate || newResponse.IssueDate,
|
||||||
EndDate: newResponse.persianEndDate || newResponse.EndDate,
|
EndDate: newResponse.persianEndDate || newResponse.EndDate,
|
||||||
|
|
||||||
// Insurance details
|
// Insurance details
|
||||||
LastCompanyDocumentNumber: newResponse.lastCompanyInsuranceNumber || newResponse.LastCompanyDocumentNumber || newResponse.insuranceNumber,
|
LastCompanyDocumentNumber:
|
||||||
|
newResponse.lastCompanyInsuranceNumber ||
|
||||||
|
newResponse.LastCompanyDocumentNumber ||
|
||||||
|
newResponse.insuranceNumber,
|
||||||
|
|
||||||
// Technical details
|
// Technical details
|
||||||
MtrNum: newResponse.MtrNum || newResponse.mtrnum,
|
MtrNum: newResponse.MtrNum || newResponse.mtrnum,
|
||||||
ShsNum: newResponse.ShsNum || newResponse.shsNam || newResponse.ChassisNumberField,
|
ShsNum:
|
||||||
|
newResponse.ShsNum ||
|
||||||
|
newResponse.shsNam ||
|
||||||
|
newResponse.ChassisNumberField,
|
||||||
vin: newResponse.vin || newResponse.VinNumberField || newResponse.vin,
|
vin: newResponse.vin || newResponse.VinNumberField || newResponse.vin,
|
||||||
VinNumberField: newResponse.vin || newResponse.VinNumberField,
|
VinNumberField: newResponse.vin || newResponse.VinNumberField,
|
||||||
ChassisNumberField: newResponse.ChassisNumberField || newResponse.shsNam,
|
ChassisNumberField: newResponse.ChassisNumberField || newResponse.shsNam,
|
||||||
@@ -422,7 +439,8 @@ export class SandHubService {
|
|||||||
// Discount information
|
// Discount information
|
||||||
DisFnYrPrcnt: newResponse.DisFnYrPrcnt || newResponse.disFnYrPrcnt || "0",
|
DisFnYrPrcnt: newResponse.DisFnYrPrcnt || newResponse.disFnYrPrcnt || "0",
|
||||||
DisLfYrPrcnt: newResponse.DisLfYrPrcnt || newResponse.disLfYrPrcnt || "0",
|
DisLfYrPrcnt: newResponse.DisLfYrPrcnt || newResponse.disLfYrPrcnt || "0",
|
||||||
DisPrsnYrPrcnt: newResponse.DisPrsnYrPrcnt || newResponse.disPrsnYrPrcnt || "0",
|
DisPrsnYrPrcnt:
|
||||||
|
newResponse.DisPrsnYrPrcnt || newResponse.disPrsnYrPrcnt || "0",
|
||||||
DisFnYrNum: newResponse.DisFnYrNum || newResponse.disFnYrNum,
|
DisFnYrNum: newResponse.DisFnYrNum || newResponse.disFnYrNum,
|
||||||
DisLfYrNum: newResponse.DisLfYrNum || newResponse.disLfYrNum,
|
DisLfYrNum: newResponse.DisLfYrNum || newResponse.disLfYrNum,
|
||||||
DisPrsnYrNum: newResponse.DisPrsnYrNum || newResponse.disPrsnYrNum,
|
DisPrsnYrNum: newResponse.DisPrsnYrNum || newResponse.disPrsnYrNum,
|
||||||
@@ -433,10 +451,13 @@ export class SandHubService {
|
|||||||
CarGroupCode: newResponse.CarGroupCode || newResponse.carGrpCod,
|
CarGroupCode: newResponse.CarGroupCode || newResponse.carGrpCod,
|
||||||
|
|
||||||
// Policy information
|
// Policy information
|
||||||
ThirdPolicyCode: newResponse.ThirdPolicyCode || newResponse.ThirdPolicyCode,
|
ThirdPolicyCode:
|
||||||
|
newResponse.ThirdPolicyCode || newResponse.ThirdPolicyCode,
|
||||||
|
|
||||||
// Endorsement data
|
// Endorsement data
|
||||||
EdrsJson: newResponse.EdrsJson || (newResponse.edrSes ? JSON.stringify(newResponse.edrSes) : undefined),
|
EdrsJson:
|
||||||
|
newResponse.EdrsJson ||
|
||||||
|
(newResponse.edrSes ? JSON.stringify(newResponse.edrSes) : undefined),
|
||||||
|
|
||||||
// Insurance fullname
|
// Insurance fullname
|
||||||
InsuranceFullName: newResponse.InsuranceFullName || newResponse.fullname,
|
InsuranceFullName: newResponse.InsuranceFullName || newResponse.fullname,
|
||||||
@@ -455,7 +476,7 @@ export class SandHubService {
|
|||||||
rightTwoDigits: String(userDetail.plate.ir),
|
rightTwoDigits: String(userDetail.plate.ir),
|
||||||
nationalCode: userDetail.nationalCodeOfInsurer,
|
nationalCode: userDetail.nationalCodeOfInsurer,
|
||||||
};
|
};
|
||||||
const base = process.env.SANDHUB_BASE_URL ?? "";
|
const base = process.env.SANHUB_BASE_URL ?? "";
|
||||||
const requestUrl = `${base}/block-inquiry-tejarat`;
|
const requestUrl = `${base}/block-inquiry-tejarat`;
|
||||||
|
|
||||||
let response: any;
|
let response: any;
|
||||||
@@ -486,12 +507,9 @@ export class SandHubService {
|
|||||||
* this codebase has the value as a Jalali date (number `13770624` or string
|
* this codebase has the value as a Jalali date (number `13770624` or string
|
||||||
* `"1377-06-24"`/`"1377/06/24"`). We convert here so callers don't have to.
|
* `"1377-06-24"`/`"1377/06/24"`). We convert here so callers don't have to.
|
||||||
*/
|
*/
|
||||||
async getPersonalInquiry(
|
async getPersonalInquiry(nationalCode: string, birthDate: number | string) {
|
||||||
nationalCode: string,
|
|
||||||
birthDate: number | string,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/personal-inquiry/tejarat-no`;
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/personal-inquiry/tejarat-no`;
|
||||||
|
|
||||||
const gregorianBirthdate = jalaliToGregorianDate(birthDate);
|
const gregorianBirthdate = jalaliToGregorianDate(birthDate);
|
||||||
if (!gregorianBirthdate) {
|
if (!gregorianBirthdate) {
|
||||||
@@ -531,7 +549,7 @@ export class SandHubService {
|
|||||||
nationalCode: string,
|
nationalCode: string,
|
||||||
driverLicenseNumber: string,
|
driverLicenseNumber: string,
|
||||||
) {
|
) {
|
||||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/driver-license-check`;
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/driver-license-check`;
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
driverLicenseNumber,
|
driverLicenseNumber,
|
||||||
nationalCode,
|
nationalCode,
|
||||||
@@ -570,7 +588,7 @@ export class SandHubService {
|
|||||||
|
|
||||||
async getCarOwnershipInfo(plate: any, nationalCode: string) {
|
async getCarOwnershipInfo(plate: any, nationalCode: string) {
|
||||||
try {
|
try {
|
||||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/ownership`;
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/ownership`;
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
Plk1: String(plate.leftDigits),
|
Plk1: String(plate.leftDigits),
|
||||||
Plk2: String(plate.centerAlphabet),
|
Plk2: String(plate.centerAlphabet),
|
||||||
@@ -606,7 +624,7 @@ export class SandHubService {
|
|||||||
|
|
||||||
async getShebaValidation(nationalId: string, shebaId: string) {
|
async getShebaValidation(nationalId: string, shebaId: string) {
|
||||||
try {
|
try {
|
||||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/sheba/sheba-tejaratno`;
|
const requestUrl = `${process.env.SANHUB_BASE_URL}/sheba/sheba-tejaratno`;
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
AccountOwnerType: "1",
|
AccountOwnerType: "1",
|
||||||
NationalId: nationalId,
|
NationalId: nationalId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { KavenegarService } from "@fraybabak/kavenegar_nest";
|
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { KavenegarService } from "./kavenegar.service";
|
||||||
import {
|
import {
|
||||||
isKavenegarSuccess,
|
isKavenegarSuccess,
|
||||||
KavenegarNormalized,
|
KavenegarNormalized,
|
||||||
@@ -21,29 +21,27 @@ export class KavenegarSmsGateway {
|
|||||||
|
|
||||||
async sendMessage(data: SendMessage) {
|
async sendMessage(data: SendMessage) {
|
||||||
try {
|
try {
|
||||||
const body = await this.sender.Send({
|
const body = await this.sender.send({
|
||||||
sender: "10008663",
|
sender: "10008663",
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isKavenegarSuccess(body)) {
|
if (!isKavenegarSuccess(body)) {
|
||||||
const normalized = normalizeKavenegarBody(body);
|
const normalized = normalizeKavenegarBody(body);
|
||||||
|
|
||||||
this.logProviderRejection("send", data.receptor, undefined, normalized);
|
this.logProviderRejection("send", data.receptor, undefined, normalized);
|
||||||
|
|
||||||
throw new SmsProviderException("send", {
|
throw new SmsProviderException("send", {
|
||||||
receptor: data.receptor,
|
receptor: data.receptor,
|
||||||
providerBody: body,
|
providerBody: body,
|
||||||
normalized,
|
normalized,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.logger.log(
|
|
||||||
`Kavenegar Send ok receptor=${data.receptor} status=200`,
|
|
||||||
);
|
|
||||||
return body;
|
return body;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SmsProviderException) throw e;
|
if (e instanceof SmsProviderException) throw e;
|
||||||
const detail = safeJsonStringify(unwrapKavenegarTransportError(e));
|
|
||||||
this.logger.error(
|
|
||||||
`Kavenegar Send transport error receptor=${data.receptor} detail=${detail}`,
|
|
||||||
);
|
|
||||||
throw new SmsTransportException("send", { receptor: data.receptor }, e);
|
throw new SmsTransportException("send", { receptor: data.receptor }, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,14 +49,17 @@ export class KavenegarSmsGateway {
|
|||||||
async verifyLookUp(data: VerifyLookUpMessage) {
|
async verifyLookUp(data: VerifyLookUpMessage) {
|
||||||
try {
|
try {
|
||||||
const body = await this.sender.verifyLookup(data);
|
const body = await this.sender.verifyLookup(data);
|
||||||
|
|
||||||
if (!isKavenegarSuccess(body)) {
|
if (!isKavenegarSuccess(body)) {
|
||||||
const normalized = normalizeKavenegarBody(body);
|
const normalized = normalizeKavenegarBody(body);
|
||||||
|
|
||||||
this.logProviderRejection(
|
this.logProviderRejection(
|
||||||
"verifyLookUp",
|
"verifyLookUp",
|
||||||
data.receptor,
|
data.receptor,
|
||||||
data.template,
|
data.template,
|
||||||
normalized,
|
normalized,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new SmsProviderException("verifyLookup", {
|
throw new SmsProviderException("verifyLookup", {
|
||||||
receptor: data.receptor,
|
receptor: data.receptor,
|
||||||
template: data.template,
|
template: data.template,
|
||||||
@@ -66,19 +67,21 @@ export class KavenegarSmsGateway {
|
|||||||
normalized,
|
normalized,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.logger.log(
|
|
||||||
`Kavenegar verifyLookUp ok receptor=${data.receptor} template=${data.template} status=200`,
|
|
||||||
);
|
|
||||||
return body;
|
return body;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SmsProviderException) throw e;
|
if (e instanceof SmsProviderException) throw e;
|
||||||
|
|
||||||
const detail = safeJsonStringify(unwrapKavenegarTransportError(e));
|
const detail = safeJsonStringify(unwrapKavenegarTransportError(e));
|
||||||
this.logger.error(
|
|
||||||
`Kavenegar verifyLookUp transport error receptor=${data.receptor} template=${data.template} detail=${detail}`,
|
this.logger.error(`Kavenegar transport error ${detail}`);
|
||||||
);
|
|
||||||
throw new SmsTransportException(
|
throw new SmsTransportException(
|
||||||
"verifyLookup",
|
"verifyLookup",
|
||||||
{ receptor: data.receptor, template: data.template },
|
{
|
||||||
|
receptor: data.receptor,
|
||||||
|
template: data.template,
|
||||||
|
},
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -90,9 +93,10 @@ export class KavenegarSmsGateway {
|
|||||||
template: string | undefined,
|
template: string | undefined,
|
||||||
normalized: KavenegarNormalized,
|
normalized: KavenegarNormalized,
|
||||||
) {
|
) {
|
||||||
const t = template != null && template !== "" ? ` template=${template}` : "";
|
const t = template != null ? ` template=${template}` : "";
|
||||||
|
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Kavenegar ${op} rejected receptor=${receptor}${t} providerStatus=${normalized.httpLikeStatus ?? "unknown"} providerMessage=${normalized.message ?? "n/a"} body=${normalized.raw}`,
|
`Kavenegar ${op} rejected receptor=${receptor}${t} providerStatus=${normalized.httpLikeStatus}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/sms-orchestration/provider/kavenegar.service.ts
Normal file
45
src/sms-orchestration/provider/kavenegar.service.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { HttpService } from "@nestjs/axios";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class KavenegarService {
|
||||||
|
private readonly apiKey: string;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly http: HttpService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.apiKey = this.configService.get<string>("SMS_API_KEY", "");
|
||||||
|
|
||||||
|
this.baseUrl = `https://api.kavenegar.com/v1/${this.apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(data: { receptor: string; message: string; sender?: string }) {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.http.post(`${this.baseUrl}/sms/send.json`, null, {
|
||||||
|
params: data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyLookup(data: {
|
||||||
|
receptor: string;
|
||||||
|
token: string;
|
||||||
|
template: string;
|
||||||
|
token2?: string;
|
||||||
|
token3?: string;
|
||||||
|
}) {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.http.get(`${this.baseUrl}/verify/lookup.json`, {
|
||||||
|
params: data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import axios, { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
import { buildParsianTemplateMessage } from "./parsian-template-messages";
|
import { buildParsianTemplateMessage } from "./parsian-template-messages";
|
||||||
import {
|
import {
|
||||||
SmsProviderException,
|
SmsProviderException,
|
||||||
SmsTransportException,
|
SmsTransportException,
|
||||||
} from "./sms-provider.exception";
|
} from "./sms-provider.exception";
|
||||||
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
|
import { SendMessage, VerifyLookUpMessage } from "./sms-gateway.types";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
import { HttpService } from "@nestjs/axios";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ParsianSmsGateway {
|
export class ParsianSmsGateway {
|
||||||
private readonly logger = new Logger(ParsianSmsGateway.name);
|
private readonly logger = new Logger(ParsianSmsGateway.name);
|
||||||
|
|
||||||
|
constructor(private readonly httpService: HttpService) {}
|
||||||
|
|
||||||
async sendMessage(data: SendMessage) {
|
async sendMessage(data: SendMessage) {
|
||||||
try {
|
try {
|
||||||
const body = await this.requestSend(data.receptor, data.message);
|
const body = await this.requestSend(data.receptor, data.message);
|
||||||
@@ -19,7 +23,11 @@ export class ParsianSmsGateway {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SmsProviderException) throw e;
|
if (e instanceof SmsProviderException) throw e;
|
||||||
const detail = isAxiosError(e)
|
const detail = isAxiosError(e)
|
||||||
? { status: e.response?.status, data: e.response?.data, message: e.message }
|
? {
|
||||||
|
status: e.response?.status,
|
||||||
|
data: e.response?.data,
|
||||||
|
message: e.message,
|
||||||
|
}
|
||||||
: e;
|
: e;
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Parsian Send transport error receptor=${data.receptor} detail=${JSON.stringify(detail)}`,
|
`Parsian Send transport error receptor=${data.receptor} detail=${JSON.stringify(detail)}`,
|
||||||
@@ -39,7 +47,11 @@ export class ParsianSmsGateway {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SmsProviderException) throw e;
|
if (e instanceof SmsProviderException) throw e;
|
||||||
const detail = isAxiosError(e)
|
const detail = isAxiosError(e)
|
||||||
? { status: e.response?.status, data: e.response?.data, message: e.message }
|
? {
|
||||||
|
status: e.response?.status,
|
||||||
|
data: e.response?.data,
|
||||||
|
message: e.message,
|
||||||
|
}
|
||||||
: e;
|
: e;
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Parsian verifyLookUp transport error receptor=${data.receptor} template=${data.template} detail=${JSON.stringify(detail)}`,
|
`Parsian verifyLookUp transport error receptor=${data.receptor} template=${data.template} detail=${JSON.stringify(detail)}`,
|
||||||
@@ -60,7 +72,10 @@ export class ParsianSmsGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestSend(receptor: string, message: string): Promise<unknown> {
|
private async requestSend(
|
||||||
|
receptor: string,
|
||||||
|
message: string,
|
||||||
|
): Promise<unknown> {
|
||||||
const baseUrl = process.env.PARSIAN_SMS_URL;
|
const baseUrl = process.env.PARSIAN_SMS_URL;
|
||||||
if (!baseUrl?.trim()) {
|
if (!baseUrl?.trim()) {
|
||||||
throw new SmsTransportException(
|
throw new SmsTransportException(
|
||||||
@@ -71,7 +86,9 @@ export class ParsianSmsGateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = `${baseUrl}=${receptor}&Message=${encodeURIComponent(message)}`;
|
const url = `${baseUrl}=${receptor}&Message=${encodeURIComponent(message)}`;
|
||||||
const response = await axios.get(url, { headers: this.parsianHeaders() });
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(url, { headers: this.parsianHeaders() }),
|
||||||
|
);
|
||||||
|
|
||||||
if (response.status < 200 || response.status >= 300) {
|
if (response.status < 200 || response.status >= 300) {
|
||||||
throw new SmsProviderException("send", {
|
throw new SmsProviderException("send", {
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
import { KavenegarModule } from "@fraybabak/kavenegar_nest";
|
import { HttpModule } from "@nestjs/axios";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import * as dotenv from "dotenv";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
|
||||||
|
import { KavenegarService } from "./kavenegar.service";
|
||||||
import { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
|
import { KavenegarSmsGateway } from "./kavenegar-sms.gateway";
|
||||||
import { ParsianSmsGateway } from "./parsian-sms.gateway";
|
import { ParsianSmsGateway } from "./parsian-sms.gateway";
|
||||||
import { SmsGatewayService } from "./sms-gateway.service";
|
import { SmsGatewayService } from "./sms-gateway.service";
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [HttpModule, ConfigModule],
|
||||||
KavenegarModule.forRoot({
|
|
||||||
apikey: process.env.SMS_API_KEY || "",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
|
KavenegarService,
|
||||||
KavenegarSmsGateway,
|
KavenegarSmsGateway,
|
||||||
ParsianSmsGateway,
|
ParsianSmsGateway,
|
||||||
SmsGatewayService,
|
SmsGatewayService,
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { SmsText, SmsTextSchema } from "./entities/schema/sms-text.schema";
|
|||||||
import { SmsGatewayModule } from "./provider/sms-gateway.module";
|
import { SmsGatewayModule } from "./provider/sms-gateway.module";
|
||||||
import { OtpGeneratorService } from "./otp-generator.service";
|
import { OtpGeneratorService } from "./otp-generator.service";
|
||||||
import { SmsOrchestrationService } from "./sms-orchestration.service";
|
import { SmsOrchestrationService } from "./sms-orchestration.service";
|
||||||
|
import { HttpModule } from "@nestjs/axios";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
HttpModule,
|
||||||
SmsGatewayModule,
|
SmsGatewayModule,
|
||||||
MongooseModule.forFeature([{ name: SmsText.name, schema: SmsTextSchema }]),
|
MongooseModule.forFeature([{ name: SmsText.name, schema: SmsTextSchema }]),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,95 +1,94 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Cron, CronExpression } from "@nestjs/schedule";
|
|
||||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||||
import { SubmitReply } from "src/request-management/entities/schema/request-management.schema";
|
import { SubmitReply } from "src/request-management/entities/schema/request-management.schema";
|
||||||
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
|
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AutoCloseRequestService {
|
export class AutoCloseRequestService {
|
||||||
private readonly logger = new Logger(AutoCloseRequestService.name);
|
// private readonly logger = new Logger(AutoCloseRequestService.name);
|
||||||
|
|
||||||
constructor(private readonly requestDb: RequestManagementDbService) {}
|
// constructor(private readonly requestDb: RequestManagementDbService) {}
|
||||||
|
|
||||||
async scheduleAutoClose(params: { requestId: string; runAt: Date }) {
|
// async scheduleAutoClose(params: { requestId: string; runAt: Date }) {
|
||||||
await this.requestDb.findAndUpdate(
|
// await this.requestDb.findAndUpdate(
|
||||||
{ _id: params.requestId },
|
// { _id: params.requestId },
|
||||||
{ $set: { autoCloseTriggerTime: params.runAt } },
|
// { $set: { autoCloseTriggerTime: params.runAt } },
|
||||||
);
|
// );
|
||||||
this.logger.log(
|
// this.logger.log(
|
||||||
`Scheduled auto-close for ${params.requestId} at ${params.runAt.toISOString()}`,
|
// `Scheduled auto-close for ${params.requestId} at ${params.runAt.toISOString()}`,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Runs every hour
|
// // Runs every hour
|
||||||
@Cron(CronExpression.EVERY_HOUR)
|
// @Cron(CronExpression.EVERY_HOUR)
|
||||||
async handleAutoClose() {
|
// async handleAutoClose() {
|
||||||
const now = new Date();
|
// const now = new Date();
|
||||||
const threshold = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
// const threshold = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const requestsToClose = await this.requestDb.findAll({
|
// const requestsToClose = await this.requestDb.findAll({
|
||||||
blameStatus: { $ne: ReqBlameStatus.CloseRequest },
|
// blameStatus: { $ne: ReqBlameStatus.CloseRequest },
|
||||||
$or: [
|
// $or: [
|
||||||
{ "expertSubmitReply.firstPartyComment.isAccept": true },
|
// { "expertSubmitReply.firstPartyComment.isAccept": true },
|
||||||
{ "expertSubmitReply.secondPartyComment.isAccept": true },
|
// { "expertSubmitReply.secondPartyComment.isAccept": true },
|
||||||
{ "expertSubmitReplyFinal.firstPartyComment.isAccept": true },
|
// { "expertSubmitReplyFinal.firstPartyComment.isAccept": true },
|
||||||
{ "expertSubmitReplyFinal.secondPartyComment.isAccept": true },
|
// { "expertSubmitReplyFinal.secondPartyComment.isAccept": true },
|
||||||
],
|
// ],
|
||||||
autoCloseTriggerTime: { $lte: threshold },
|
// autoCloseTriggerTime: { $lte: threshold },
|
||||||
});
|
// });
|
||||||
|
|
||||||
for (const request of requestsToClose) {
|
// for (const request of requestsToClose) {
|
||||||
const reply = request.expertSubmitReplyFinal || request.expertSubmitReply;
|
// const reply = request.expertSubmitReplyFinal || request.expertSubmitReply;
|
||||||
|
|
||||||
const isSubmitReply = (reply: any): reply is SubmitReply =>
|
// const isSubmitReply = (reply: any): reply is SubmitReply =>
|
||||||
reply &&
|
// reply &&
|
||||||
typeof reply === "object" &&
|
// typeof reply === "object" &&
|
||||||
("firstPartyComment" in reply || "secondPartyComment" in reply);
|
// ("firstPartyComment" in reply || "secondPartyComment" in reply);
|
||||||
|
|
||||||
if (isSubmitReply(reply)) {
|
// if (isSubmitReply(reply)) {
|
||||||
const firstAccepted = reply.firstPartyComment?.isAccept;
|
// const firstAccepted = reply.firstPartyComment?.isAccept;
|
||||||
const secondAccepted = reply.secondPartyComment?.isAccept;
|
// const secondAccepted = reply.secondPartyComment?.isAccept;
|
||||||
|
|
||||||
if (!firstAccepted || !secondAccepted) {
|
// if (!firstAccepted || !secondAccepted) {
|
||||||
await this.requestDb.findByIdAndUpdate(request._id.toString(), {
|
// await this.requestDb.findByIdAndUpdate(request._id.toString(), {
|
||||||
blameStatus: ReqBlameStatus.CloseRequest,
|
// blameStatus: ReqBlameStatus.CloseRequest,
|
||||||
});
|
// });
|
||||||
this.logger.log(`Auto-closed request ${request._id}`);
|
// this.logger.log(`Auto-closed request ${request._id}`);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_2AM)
|
// @Cron(CronExpression.EVERY_DAY_AT_2AM)
|
||||||
async handleBlameFileCleanup() {
|
// async handleBlameFileCleanup() {
|
||||||
this.logger.log("Running scheduled blame file cleanup job...");
|
// this.logger.log("Running scheduled blame file cleanup job...");
|
||||||
|
|
||||||
const sevenDaysAgo = new Date();
|
// const sevenDaysAgo = new Date();
|
||||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
// sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||||
|
|
||||||
const staleStatuses = [
|
// const staleStatuses = [
|
||||||
ReqBlameStatus.PendingForFirstParty,
|
// ReqBlameStatus.PendingForFirstParty,
|
||||||
ReqBlameStatus.PendingForSecondParty,
|
// ReqBlameStatus.PendingForSecondParty,
|
||||||
];
|
// ];
|
||||||
|
|
||||||
const query = {
|
// const query = {
|
||||||
createdAt: { $lt: sevenDaysAgo },
|
// createdAt: { $lt: sevenDaysAgo },
|
||||||
blameStatus: { $in: staleStatuses },
|
// blameStatus: { $in: staleStatuses },
|
||||||
};
|
// };
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const result = await this.requestDb.updateMany(query, {
|
// const result = await this.requestDb.updateMany(query, {
|
||||||
$set: { blameStatus: ReqBlameStatus.CloseRequest },
|
// $set: { blameStatus: ReqBlameStatus.CloseRequest },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (result.modifiedCount > 0) {
|
// if (result.modifiedCount > 0) {
|
||||||
this.logger.log(
|
// this.logger.log(
|
||||||
`Successfully closed ${result.modifiedCount} stale blame files.`,
|
// `Successfully closed ${result.modifiedCount} stale blame files.`,
|
||||||
);
|
// );
|
||||||
} else {
|
// } else {
|
||||||
this.logger.log("No stale blame files found to close.");
|
// this.logger.log("No stale blame files found to close.");
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
this.logger.error("Error during scheduled blame file cleanup:", error);
|
// this.logger.error("Error during scheduled blame file cleanup:", error);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user