forked from Yara724/api
Toggle External API
This commit is contained in:
@@ -16,6 +16,7 @@ import { LookupsModule } from "./lookups/lookups.module";
|
||||
import { PlatesModule } from "./plates/plates.module";
|
||||
import { ProfileModule } from "./profile/profile.module";
|
||||
import { SandHubModule } from "./sand-hub/sand-hub.module";
|
||||
import { SystemSettingsModule } from "./system-settings/system-settings.module";
|
||||
import { ReportsModule } from "./reports/reports.module";
|
||||
import { RequestManagementModule } from "./request-management/request-management.module";
|
||||
import { UsersModule } from "./users/users.module";
|
||||
@@ -58,6 +59,7 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
||||
PlatesModule,
|
||||
RequestManagementModule,
|
||||
SandHubModule,
|
||||
SystemSettingsModule,
|
||||
ExpertBlameModule,
|
||||
ClaimRequestManagementModule,
|
||||
ExpertClaimModule,
|
||||
|
||||
41
src/auth/guards/settings-jwt.guard.ts
Normal file
41
src/auth/guards/settings-jwt.guard.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Request } from "express";
|
||||
|
||||
/**
|
||||
* Verifies Bearer JWT for platform settings routes. Does not restrict by role;
|
||||
* pair with {@link RolesGuard} on handlers.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SettingsJwtGuard implements CanActivate {
|
||||
constructor(private readonly jwtService: JwtService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException("Token not found");
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: `${process.env.SECRET}`,
|
||||
});
|
||||
(request as any).user = payload;
|
||||
(request as any).identity = payload;
|
||||
return true;
|
||||
} catch {
|
||||
throw new UnauthorizedException("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(" ") ?? [];
|
||||
return type === "Bearer" ? token : undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { MongooseModule } from "@nestjs/mongoose";
|
||||
import { SystemSettingsModule } from "src/system-settings/system-settings.module";
|
||||
import { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service";
|
||||
import { SandHubModel, SandHubSchema } from "./entity/schema/sand-hub.schema";
|
||||
import { SandHubService } from "./sand-hub.service";
|
||||
@@ -8,6 +9,7 @@ import { SandHubService } from "./sand-hub.service";
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule,
|
||||
SystemSettingsModule,
|
||||
MongooseModule.forFeature([
|
||||
{ name: SandHubModel.name, schema: SandHubSchema },
|
||||
]),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import axios from "axios";
|
||||
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 { SystemSettingsService } from "src/system-settings/system-settings.service";
|
||||
import { SandHubDetailDto } from "./dto/sand-hub.dto";
|
||||
import { jalaliToGregorianDate } from "src/helpers/date-jalali";
|
||||
|
||||
@@ -28,14 +29,15 @@ export class SandHubService {
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly sandHubDbService: SandHubDbService,
|
||||
private readonly systemSettingsService: SystemSettingsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* When false (default), no outbound HTTP to SandHub/Tejarat gateways — all inquiries return mocks.
|
||||
* Set `SANDHUB_USE_REAL_API=true` to restore live calls.
|
||||
* When false, no outbound HTTP to SandHub/Tejarat — inquiries use permissive mocks.
|
||||
* Controlled by `system_settings.externalApis.sandHubUseLiveApi` (PATCH /system-settings).
|
||||
*/
|
||||
private useLiveSandHubApis(): boolean {
|
||||
return process.env.SANDHUB_USE_REAL_API === "true";
|
||||
private async useLiveSandHubApis(): Promise<boolean> {
|
||||
return this.systemSettingsService.isSandHubLiveEnabled();
|
||||
}
|
||||
|
||||
/** Fixed plate/insurance inquiry payload used everywhere we mock block-inquiry style APIs. */
|
||||
@@ -117,9 +119,12 @@ export class SandHubService {
|
||||
/**
|
||||
* Bodies returned from `makeSandHubRequest` in mock mode (same shape callers expect from real API).
|
||||
*/
|
||||
private buildMockSandHubResponseForUrl(url: string): any {
|
||||
private buildMockSandHubResponseForUrl(url: string, payload?: unknown): any {
|
||||
const u = (url || "").toLowerCase();
|
||||
if (u.includes("personal-inquiry")) {
|
||||
const p = payload as { nationalCode?: string } | undefined;
|
||||
const nin =
|
||||
typeof p?.nationalCode === "string" ? p.nationalCode : "-";
|
||||
return {
|
||||
message: "success",
|
||||
data: {
|
||||
@@ -127,7 +132,7 @@ export class SandHubService {
|
||||
lastName: "خانوادگی",
|
||||
fatherName: "-",
|
||||
birthCertificateNumber: "-",
|
||||
nin: "-",
|
||||
nin,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -165,7 +170,7 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private async getAccessToken(): Promise<string> {
|
||||
if (!this.useLiveSandHubApis()) {
|
||||
if (!(await this.useLiveSandHubApis())) {
|
||||
return "mock-sandhub-access-token";
|
||||
}
|
||||
if (this.loginToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
||||
@@ -205,7 +210,7 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private async getTejaratAccessToken(): Promise<string> {
|
||||
if (!this.useLiveSandHubApis()) {
|
||||
if (!(await this.useLiveSandHubApis())) {
|
||||
return "mock-tejarat-access-token";
|
||||
}
|
||||
if (
|
||||
@@ -263,7 +268,7 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private async makeTejaratRequest(url: string, payload: any, maxRetries = 2) {
|
||||
if (!this.useLiveSandHubApis()) {
|
||||
if (!(await this.useLiveSandHubApis())) {
|
||||
this.logger.log(`[MOCK] Tejarat POST skipped: ${url}`);
|
||||
return this.getDefaultMockPlateInquiryRaw();
|
||||
}
|
||||
@@ -328,10 +333,11 @@ export class SandHubService {
|
||||
};
|
||||
|
||||
const requestUrl = `${baseUrl}/block-inquiry-tejarat`;
|
||||
const raw = this.useLiveSandHubApis()
|
||||
const live = await this.useLiveSandHubApis();
|
||||
const raw = live
|
||||
? await this.makeTejaratRequest(requestUrl, requestPayload)
|
||||
: this.getDefaultMockPlateInquiryRaw();
|
||||
if (!this.useLiveSandHubApis()) {
|
||||
if (!live) {
|
||||
this.logger.debug(
|
||||
`[MOCK] getTejaratBlockInquiry plate=${JSON.stringify(requestPayload)}`,
|
||||
);
|
||||
@@ -345,9 +351,9 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private async makeSandHubRequest(url: string, payload: any, maxRetries = 3) {
|
||||
if (!this.useLiveSandHubApis()) {
|
||||
if (!(await this.useLiveSandHubApis())) {
|
||||
this.logger.log(`[MOCK] SandHub POST skipped: ${url}`);
|
||||
return this.buildMockSandHubResponseForUrl(url);
|
||||
return this.buildMockSandHubResponseForUrl(url, payload);
|
||||
}
|
||||
const token = await this.getAccessToken();
|
||||
const INITIAL_DELAY = 1000;
|
||||
@@ -453,7 +459,7 @@ export class SandHubService {
|
||||
const requestUrl = `${base}/block-inquiry-tejarat`;
|
||||
|
||||
let response: any;
|
||||
if (this.useLiveSandHubApis()) {
|
||||
if (await this.useLiveSandHubApis()) {
|
||||
response = await this.makeSandHubRequest(requestUrl, requestPayload);
|
||||
} else {
|
||||
this.logger.debug(
|
||||
|
||||
46
src/system-settings/dto/system-settings.dto.ts
Normal file
46
src/system-settings/dto/system-settings.dto.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
export class ExternalApisSettingsDto {
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"When true, SandHub/Tejarat inquiry HTTP APIs are called. When false, mocks are used and validations pass.",
|
||||
example: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
sandHubUseLiveApi?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateSystemSettingsDto {
|
||||
@ApiPropertyOptional({ type: ExternalApisSettingsDto })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => ExternalApisSettingsDto)
|
||||
externalApis?: ExternalApisSettingsDto;
|
||||
}
|
||||
|
||||
export class ExternalApisSettingsViewDto {
|
||||
@ApiProperty({
|
||||
description:
|
||||
"Stored in `system_settings.externalApis.sandHubUseLiveApi` — controls SandHubService",
|
||||
example: false,
|
||||
})
|
||||
sandHubUseLiveApi: boolean;
|
||||
}
|
||||
|
||||
export class SystemSettingsResponseDto {
|
||||
@ApiProperty({ example: "global" })
|
||||
key: string;
|
||||
|
||||
@ApiProperty({ type: ExternalApisSettingsViewDto })
|
||||
externalApis: ExternalApisSettingsViewDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Human-readable mode for operators",
|
||||
example: "mock",
|
||||
enum: ["live", "mock"],
|
||||
})
|
||||
sandHubMode: "live" | "mock";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectModel } from "@nestjs/mongoose";
|
||||
import { Model, UpdateQuery } from "mongoose";
|
||||
import {
|
||||
SYSTEM_SETTINGS_GLOBAL_KEY,
|
||||
SystemSettingsModel,
|
||||
} from "../schema/system-settings.schema";
|
||||
|
||||
@Injectable()
|
||||
export class SystemSettingsDbService {
|
||||
constructor(
|
||||
@InjectModel(SystemSettingsModel.name)
|
||||
private readonly model: Model<SystemSettingsModel>,
|
||||
) {}
|
||||
|
||||
findGlobal() {
|
||||
return this.model
|
||||
.findOne({ key: SYSTEM_SETTINGS_GLOBAL_KEY })
|
||||
.lean()
|
||||
.exec();
|
||||
}
|
||||
|
||||
async upsertGlobal(update: UpdateQuery<SystemSettingsModel>) {
|
||||
return this.model
|
||||
.findOneAndUpdate(
|
||||
{ key: SYSTEM_SETTINGS_GLOBAL_KEY },
|
||||
{
|
||||
$setOnInsert: { key: SYSTEM_SETTINGS_GLOBAL_KEY },
|
||||
...update,
|
||||
},
|
||||
{ new: true, upsert: true, lean: true },
|
||||
)
|
||||
.exec();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
|
||||
/** Singleton document key — only `global` is used today. */
|
||||
export const SYSTEM_SETTINGS_GLOBAL_KEY = "global";
|
||||
|
||||
/**
|
||||
* Toggle outbound integrations (SandHub / Tejarat inquiry gateways, etc.).
|
||||
* When `sandHubUseLiveApi` is false, {@link SandHubService} skips HTTP and
|
||||
* returns permissive mocks (including default plate/insurance inquiry data).
|
||||
*/
|
||||
export class ExternalApisSettings {
|
||||
/**
|
||||
* `true` → live SandHub/Tejarat HTTP calls.
|
||||
* `false` → no external requests; mocks pass validation checks.
|
||||
*/
|
||||
@Prop({ type: Boolean, required: false, default: false })
|
||||
sandHubUseLiveApi?: boolean;
|
||||
}
|
||||
|
||||
@Schema({ collection: "system_settings", versionKey: false })
|
||||
export class SystemSettingsModel {
|
||||
@Prop({ required: true, unique: true, default: SYSTEM_SETTINGS_GLOBAL_KEY })
|
||||
key: string;
|
||||
|
||||
@Prop({ type: ExternalApisSettings, required: false, default: {} })
|
||||
externalApis?: ExternalApisSettings;
|
||||
}
|
||||
|
||||
export const SystemSettingsSchema =
|
||||
SchemaFactory.createForClass(SystemSettingsModel);
|
||||
51
src/system-settings/system-settings.controller.ts
Normal file
51
src/system-settings/system-settings.controller.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Body, Controller, Get, Patch, UseGuards } from "@nestjs/common";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { SettingsJwtGuard } from "src/auth/guards/settings-jwt.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import {
|
||||
SystemSettingsResponseDto,
|
||||
UpdateSystemSettingsDto,
|
||||
} from "./dto/system-settings.dto";
|
||||
import { SystemSettingsService } from "./system-settings.service";
|
||||
|
||||
@ApiTags("system-settings")
|
||||
@ApiBearerAuth()
|
||||
@Controller("system-settings")
|
||||
export class SystemSettingsController {
|
||||
constructor(private readonly systemSettingsService: SystemSettingsService) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(SettingsJwtGuard, RolesGuard)
|
||||
@Roles(RoleEnum.ADMIN, RoleEnum.COMPANY)
|
||||
@ApiOperation({
|
||||
summary: "Get global system settings (external API toggles)",
|
||||
description:
|
||||
"Shows whether SandHub/Tejarat live HTTP is enabled. When disabled, the API uses mocks so flows continue without external connectivity.",
|
||||
})
|
||||
@ApiResponse({ status: 200, type: SystemSettingsResponseDto })
|
||||
getSettings(): Promise<SystemSettingsResponseDto> {
|
||||
return this.systemSettingsService.getSettingsView();
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@UseGuards(SettingsJwtGuard, RolesGuard)
|
||||
@Roles(RoleEnum.ADMIN)
|
||||
@ApiOperation({
|
||||
summary: "Update global system settings",
|
||||
description:
|
||||
"Set `externalApis.sandHubUseLiveApi` to true for live inquiries, false for mock/offline mode.",
|
||||
})
|
||||
@ApiResponse({ status: 200, type: SystemSettingsResponseDto })
|
||||
updateSettings(
|
||||
@Body() body: UpdateSystemSettingsDto,
|
||||
): Promise<SystemSettingsResponseDto> {
|
||||
return this.systemSettingsService.updateSettings(body);
|
||||
}
|
||||
}
|
||||
21
src/system-settings/system-settings.module.ts
Normal file
21
src/system-settings/system-settings.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { MongooseModule } from "@nestjs/mongoose";
|
||||
import { SystemSettingsDbService } from "./entities/db-service/system-settings.db.service";
|
||||
import {
|
||||
SystemSettingsModel,
|
||||
SystemSettingsSchema,
|
||||
} from "./entities/schema/system-settings.schema";
|
||||
import { SystemSettingsController } from "./system-settings.controller";
|
||||
import { SystemSettingsService } from "./system-settings.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
{ name: SystemSettingsModel.name, schema: SystemSettingsSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [SystemSettingsController],
|
||||
providers: [SystemSettingsService, SystemSettingsDbService],
|
||||
exports: [SystemSettingsService],
|
||||
})
|
||||
export class SystemSettingsModule {}
|
||||
31
src/system-settings/system-settings.service.spec.ts
Normal file
31
src/system-settings/system-settings.service.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { SystemSettingsService } from "./system-settings.service";
|
||||
|
||||
describe("SystemSettingsService", () => {
|
||||
const db = {
|
||||
findGlobal: jest.fn(),
|
||||
upsertGlobal: jest.fn(),
|
||||
};
|
||||
|
||||
let service: SystemSettingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = new SystemSettingsService(db as any);
|
||||
});
|
||||
|
||||
it("defaults to mock when DB field is false", async () => {
|
||||
db.findGlobal.mockResolvedValue({
|
||||
key: "global",
|
||||
externalApis: { sandHubUseLiveApi: false },
|
||||
});
|
||||
await expect(service.isSandHubLiveEnabled()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("uses live mode when DB field is true", async () => {
|
||||
db.findGlobal.mockResolvedValue({
|
||||
key: "global",
|
||||
externalApis: { sandHubUseLiveApi: true },
|
||||
});
|
||||
await expect(service.isSandHubLiveEnabled()).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
79
src/system-settings/system-settings.service.ts
Normal file
79
src/system-settings/system-settings.service.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import {
|
||||
SystemSettingsResponseDto,
|
||||
UpdateSystemSettingsDto,
|
||||
} from "./dto/system-settings.dto";
|
||||
import { SystemSettingsDbService } from "./entities/db-service/system-settings.db.service";
|
||||
|
||||
@Injectable()
|
||||
export class SystemSettingsService {
|
||||
private readonly logger = new Logger(SystemSettingsService.name);
|
||||
private cache: Record<string, unknown> | null = null;
|
||||
private cacheAt = 0;
|
||||
private readonly cacheTtlMs = 15_000;
|
||||
|
||||
constructor(private readonly db: SystemSettingsDbService) {}
|
||||
|
||||
private async loadGlobal(): Promise<Record<string, unknown>> {
|
||||
const now = Date.now();
|
||||
if (this.cache && now - this.cacheAt < this.cacheTtlMs) {
|
||||
return this.cache;
|
||||
}
|
||||
let doc = await this.db.findGlobal();
|
||||
if (!doc) {
|
||||
doc = await this.db.upsertGlobal({
|
||||
$set: { externalApis: { sandHubUseLiveApi: false } },
|
||||
});
|
||||
this.logger.log(
|
||||
"Created default system_settings document (sandHubUseLiveApi=false)",
|
||||
);
|
||||
}
|
||||
this.cache = doc;
|
||||
this.cacheAt = now;
|
||||
return doc;
|
||||
}
|
||||
|
||||
private invalidateCache(): void {
|
||||
this.cache = null;
|
||||
this.cacheAt = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether SandHub/Tejarat live HTTP should run (`system_settings` collection).
|
||||
*/
|
||||
async isSandHubLiveEnabled(): Promise<boolean> {
|
||||
const doc = await this.loadGlobal();
|
||||
const externalApis = doc.externalApis as
|
||||
| { sandHubUseLiveApi?: boolean }
|
||||
| undefined;
|
||||
return externalApis?.sandHubUseLiveApi === true;
|
||||
}
|
||||
|
||||
async getSettingsView(): Promise<SystemSettingsResponseDto> {
|
||||
const doc = await this.loadGlobal();
|
||||
const externalApis = doc.externalApis as
|
||||
| { sandHubUseLiveApi?: boolean }
|
||||
| undefined;
|
||||
const sandHubUseLiveApi = externalApis?.sandHubUseLiveApi === true;
|
||||
return {
|
||||
key: String(doc.key ?? "global"),
|
||||
externalApis: { sandHubUseLiveApi },
|
||||
sandHubMode: sandHubUseLiveApi ? "live" : "mock",
|
||||
};
|
||||
}
|
||||
|
||||
async updateSettings(
|
||||
body: UpdateSystemSettingsDto,
|
||||
): Promise<SystemSettingsResponseDto> {
|
||||
const $set: Record<string, unknown> = {};
|
||||
if (body.externalApis?.sandHubUseLiveApi !== undefined) {
|
||||
$set["externalApis.sandHubUseLiveApi"] = body.externalApis.sandHubUseLiveApi;
|
||||
}
|
||||
if (Object.keys($set).length > 0) {
|
||||
await this.db.upsertGlobal({ $set });
|
||||
this.invalidateCache();
|
||||
this.logger.log(`System settings updated: ${JSON.stringify($set)}`);
|
||||
}
|
||||
return this.getSettingsView();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user