Added Joi for schema validation, tidied up envs

This commit is contained in:
SepehrYahyaee
2026-05-30 10:37:23 +03:30
parent 10df869efb
commit 6ac0bf060e
12 changed files with 176 additions and 59 deletions

73
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"express": "^5.2.1", "express": "^5.2.1",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
"joi": "^18.2.1",
"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",
@@ -1043,6 +1044,54 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "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": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -3114,6 +3163,12 @@
"@sinonjs/commons": "^3.0.0" "@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": { "node_modules/@swc/cli": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.8.1.tgz", "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" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -38,6 +38,7 @@
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"express": "^5.2.1", "express": "^5.2.1",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
"joi": "^18.2.1",
"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",

View File

@@ -1,7 +1,7 @@
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 { ServeStaticModule } from "@nestjs/serve-static"; 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 { 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";
@Module({ @Module({
imports: [ 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, 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,
pass: process.env.MONGO_PASS,
authMechanism: "SCRAM-SHA-256",
tls: true, tls: true,
tlsAllowInvalidCertificates: true, tlsAllowInvalidCertificates: true,
autoIndex: configService.get<string>("NODE_ENV") !== "production",
connectionFactory: (connection) => { connectionFactory: (connection) => {
applyIranFaTimestampPlugin(connection); applyIranFaTimestampPlugin(connection);
return connection; return connection;
}, },
};
}, },
), }),
UsersModule, UsersModule,
AuthModule, AuthModule,
ClientModule, ClientModule,

View File

@@ -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;
} }
@@ -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");

View File

@@ -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}`,

View File

@@ -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: [

View File

@@ -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 (

View File

@@ -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;
} }
} }

View File

@@ -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();
} }

View File

@@ -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;

View File

@@ -39,9 +39,9 @@ async function bootstrap() {
.split(":"); .split(":");
const password = rest.join(":"); const password = rest.join(":");
const expectedUser = configService.get<string>("SWAGGER_USER", ""); const expectedUser = configService.get<string>("SWAGGER_USER_DEV", "");
const expectedPassword = configService.get<string>( const expectedPassword = configService.get<string>(
"SWAGGER_PASSWORD", "SWAGGER_PASSWORD_DEV",
"", "",
); );
@@ -59,7 +59,7 @@ async function bootstrap() {
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()

View File

@@ -182,9 +182,9 @@ export class SandHubService {
try { try {
const response = await firstValueFrom( const response = await firstValueFrom(
this.httpService.post(process.env.SANDHUB_URL_LOGIN, { this.httpService.post(process.env.SANHUB_URL_LOGIN, {
email: process.env.SANDHUB_USERNAME, email: process.env.SANHUB_USERNAME,
password: process.env.SANDHUB_PASSWORD, password: process.env.SANHUB_PASSWORD,
}), }),
); );
@@ -476,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;
@@ -509,7 +509,7 @@ export class SandHubService {
*/ */
async getPersonalInquiry(nationalCode: string, birthDate: number | string) { async getPersonalInquiry(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) {
@@ -549,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,
@@ -588,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),
@@ -624,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,