diff --git a/package-lock.json b/package-lock.json index 9c8dd4d..b7abe52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "class-validator": "^0.15.1", "express": "^5.2.1", "fastest-levenshtein": "^1.0.16", + "joi": "^18.2.1", "mongoose": "^8.9.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -1043,6 +1044,54 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -3114,6 +3163,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@swc/cli": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.8.1.tgz", @@ -8754,6 +8809,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", + "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 12c3d53..b92a912 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "class-validator": "^0.15.1", "express": "^5.2.1", "fastest-levenshtein": "^1.0.16", + "joi": "^18.2.1", "mongoose": "^8.9.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/src/app.module.ts b/src/app.module.ts index 8fe3843..e8c4149 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; -import { APP_INTERCEPTOR } from "@nestjs/core"; -import { Module } from "@nestjs/common"; -import { ConfigModule } from "@nestjs/config"; +import { APP_INTERCEPTOR, APP_PIPE } from "@nestjs/core"; +import { Module, ValidationPipe } from "@nestjs/common"; +import { ConfigModule, ConfigService } from "@nestjs/config"; import { UnicodeDigitsNormalizeInterceptor } from "./common/interceptors/unicode-digits-normalize.interceptor"; import { MongooseModule } from "@nestjs/mongoose"; import { ServeStaticModule } from "@nestjs/serve-static"; @@ -23,31 +23,77 @@ import { UsersModule } from "./users/users.module"; import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin"; import { CronModule } from "./utils/cron/cron.module"; import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module"; +import * as Joi from "joi"; @Module({ imports: [ - ConfigModule.forRoot({ isGlobal: true }), + ConfigModule.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, ServeStaticModule.forRoot({ rootPath: join(__dirname, "..", "files"), serveRoot: "/files", }), - MongooseModule.forRoot( - `mongodb://${process.env.MONGO_URL}:${process.env.MONGO_PORT}/`, - { - dbName: "yara724", - autoIndex: true, - user: process.env.MONGO_USER, - pass: process.env.MONGO_PASS, - authMechanism: "SCRAM-SHA-256", - tls: true, - tlsAllowInvalidCertificates: true, - connectionFactory: (connection) => { - applyIranFaTimestampPlugin(connection); - return connection; - }, + MongooseModule.forRootAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => { + return { + uri: `mongodb://${configService.get("MONGO_USER")}:${configService.get("MONGO_PASS")}@${configService.get("MONGO_HOST")}:${configService.get("MONGO_PORT")}/${configService.get("MONGO_DB_NAME")}?authSource=admin&${configService.get("MONGO_OPTIONS")}`, + tls: true, + tlsAllowInvalidCertificates: true, + autoIndex: configService.get("NODE_ENV") !== "production", + connectionFactory: (connection) => { + applyIranFaTimestampPlugin(connection); + return connection; + }, + }; }, - ), + }), UsersModule, AuthModule, ClientModule, diff --git a/src/auth/auth-services/actor.auth.service.ts b/src/auth/auth-services/actor.auth.service.ts index 9d0a92e..4cc4f5f 100644 --- a/src/auth/auth-services/actor.auth.service.ts +++ b/src/auth/auth-services/actor.auth.service.ts @@ -78,8 +78,7 @@ export class ActorAuthService { res = await this.fieldExpertDbService.findOne({ _id: new Types.ObjectId(userId), }); - else - res = await this.fieldExpertDbService.findOne({ email: username }); + else res = await this.fieldExpertDbService.findOne({ email: username }); break; case RoleEnum.REGISTRAR: if (username == null && userId) @@ -139,7 +138,7 @@ export class ActorAuthService { }; const access_token = this.jwtService.sign(payload, { - secret: `${process.env.SECRET}`, + secret: `${process.env.JWT_SECRET}`, expiresIn: "1h", }); @@ -159,9 +158,7 @@ export class ActorAuthService { throw new UnauthorizedException("user not assigned to this role"); } if (!(await this.hashService.compare(pass, user.password))) { - throw new UnauthorizedException( - "password is incorrect or access Denied", - ); + throw new UnauthorizedException("password is incorrect or access Denied"); } return user; } @@ -225,7 +222,7 @@ export class ActorAuthService { firstName: body.firstName, lastName: body.lastName, phone: body.phone, - mobile:body.mobile, + mobile: body.mobile, city: body.city, state: body.state, address: body.address, @@ -445,13 +442,7 @@ export class ActorAuthService { "state", "address", ], - field_expert: [ - "firstName", - "lastName", - "email", - "phone", - "mobile", - ], + field_expert: ["firstName", "lastName", "email", "phone", "mobile"], registrar: ["email"], }; @@ -477,7 +468,11 @@ export class ActorAuthService { } // 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"); diff --git a/src/auth/auth-services/user.auth.service.ts b/src/auth/auth-services/user.auth.service.ts index ae28ee0..c6b505e 100644 --- a/src/auth/auth-services/user.auth.service.ts +++ b/src/auth/auth-services/user.auth.service.ts @@ -75,7 +75,7 @@ export class UserAuthService { role: "user", }; const accToken = this.jwtService.sign(payload, { - secret: `${process.env.SECRET}`, + secret: `${process.env.JWT_SECRET}`, }); await this.userDbService.findOneAndUpdate( { username: user.username }, @@ -162,10 +162,7 @@ export class UserAuthService { process.env.AUTH_SMS_TEMPLATE, ); if (!ok) { - throw new HttpException( - "auth sms send failed", - HttpStatus.BAD_GATEWAY, - ); + throw new HttpException("auth sms send failed", HttpStatus.BAD_GATEWAY); } this.logger.log( `Auth OTP SMS accepted by provider phone=${mobile} otp=${otp}`, diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 55709d7..5a5de8b 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -49,9 +49,9 @@ import { CaptchaModule } from "src/captcha/captcha.module"; { name: ClaimCase.name, schema: ClaimCaseSchema }, ]), JwtModule.register({ - signOptions: { expiresIn: "1h" }, + signOptions: { expiresIn: "1h" }, // TODO: MAKE IT ENV global: true, - secret: `${process.env.SECRET}`, + secret: `${process.env.JWT_SECRET}`, }), ], providers: [ diff --git a/src/auth/guards/actor-local.guard.ts b/src/auth/guards/actor-local.guard.ts index 29b6405..1a67f69 100644 --- a/src/auth/guards/actor-local.guard.ts +++ b/src/auth/guards/actor-local.guard.ts @@ -33,7 +33,7 @@ export class LocalActorAuthGuard implements CanActivate { try { const payload = await this.jwtService.verifyAsync(token, { - secret: `${process.env.SECRET}`, + secret: `${process.env.JWT_SECRET}`, }); if ( diff --git a/src/auth/guards/claim-access.guard.ts b/src/auth/guards/claim-access.guard.ts index ce40a69..d780a18 100644 --- a/src/auth/guards/claim-access.guard.ts +++ b/src/auth/guards/claim-access.guard.ts @@ -31,7 +31,7 @@ export class ClaimAccessGuard implements CanActivate { try { 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) @@ -81,7 +81,10 @@ export class ClaimAccessGuard implements CanActivate { throw new UnauthorizedException("Invalid role"); } catch (error) { - if (error instanceof ForbiddenException || error instanceof UnauthorizedException) { + if ( + error instanceof ForbiddenException || + error instanceof UnauthorizedException + ) { throw error; } throw new UnauthorizedException(); @@ -124,4 +127,3 @@ export class ClaimAccessGuard implements CanActivate { return type === "Bearer" ? token : undefined; } } - diff --git a/src/auth/guards/global.guard.ts b/src/auth/guards/global.guard.ts index bbe2e97..80e3b8d 100644 --- a/src/auth/guards/global.guard.ts +++ b/src/auth/guards/global.guard.ts @@ -22,10 +22,13 @@ export class GlobalGuard implements CanActivate { try { 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(); } diff --git a/src/auth/guards/settings-jwt.guard.ts b/src/auth/guards/settings-jwt.guard.ts index e6c7ff8..9bfe482 100644 --- a/src/auth/guards/settings-jwt.guard.ts +++ b/src/auth/guards/settings-jwt.guard.ts @@ -24,7 +24,7 @@ export class SettingsJwtGuard implements CanActivate { try { const payload = await this.jwtService.verifyAsync(token, { - secret: `${process.env.SECRET}`, + secret: `${process.env.JWT_SECRET}`, }); (request as any).user = payload; (request as any).identity = payload; diff --git a/src/main.ts b/src/main.ts index 5e15466..ad54591 100644 --- a/src/main.ts +++ b/src/main.ts @@ -39,9 +39,9 @@ async function bootstrap() { .split(":"); const password = rest.join(":"); - const expectedUser = configService.get("SWAGGER_USER", ""); + const expectedUser = configService.get("SWAGGER_USER_DEV", ""); const expectedPassword = configService.get( - "SWAGGER_PASSWORD", + "SWAGGER_PASSWORD_DEV", "", ); @@ -59,7 +59,7 @@ async function bootstrap() { const config = new DocumentBuilder() .setTitle("yara724-backend") .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://localhost:9001") .addBearerAuth() diff --git a/src/sand-hub/sand-hub.service.ts b/src/sand-hub/sand-hub.service.ts index 146caac..0db5484 100644 --- a/src/sand-hub/sand-hub.service.ts +++ b/src/sand-hub/sand-hub.service.ts @@ -182,9 +182,9 @@ export class SandHubService { try { const response = await firstValueFrom( - this.httpService.post(process.env.SANDHUB_URL_LOGIN, { - email: process.env.SANDHUB_USERNAME, - password: process.env.SANDHUB_PASSWORD, + this.httpService.post(process.env.SANHUB_URL_LOGIN, { + email: process.env.SANHUB_USERNAME, + password: process.env.SANHUB_PASSWORD, }), ); @@ -476,7 +476,7 @@ export class SandHubService { rightTwoDigits: String(userDetail.plate.ir), nationalCode: userDetail.nationalCodeOfInsurer, }; - const base = process.env.SANDHUB_BASE_URL ?? ""; + const base = process.env.SANHUB_BASE_URL ?? ""; const requestUrl = `${base}/block-inquiry-tejarat`; let response: any; @@ -509,7 +509,7 @@ export class SandHubService { */ async getPersonalInquiry(nationalCode: string, birthDate: number | string) { 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); if (!gregorianBirthdate) { @@ -549,7 +549,7 @@ export class SandHubService { nationalCode: 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 = { driverLicenseNumber, nationalCode, @@ -588,7 +588,7 @@ export class SandHubService { async getCarOwnershipInfo(plate: any, nationalCode: string) { try { - const requestUrl = `${process.env.SANDHUB_BASE_URL}/ownership`; + const requestUrl = `${process.env.SANHUB_BASE_URL}/ownership`; const requestPayload = { Plk1: String(plate.leftDigits), Plk2: String(plate.centerAlphabet), @@ -624,7 +624,7 @@ export class SandHubService { async getShebaValidation(nationalId: string, shebaId: string) { try { - const requestUrl = `${process.env.SANDHUB_BASE_URL}/sheba/sheba-tejaratno`; + const requestUrl = `${process.env.SANHUB_BASE_URL}/sheba/sheba-tejaratno`; const requestPayload = { AccountOwnerType: "1", NationalId: nationalId,