forked from Yara724/api
Merge pull request 'Toggle External API' (#71) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#71
This commit is contained in:
@@ -16,6 +16,7 @@ import { LookupsModule } from "./lookups/lookups.module";
|
|||||||
import { PlatesModule } from "./plates/plates.module";
|
import { PlatesModule } from "./plates/plates.module";
|
||||||
import { ProfileModule } from "./profile/profile.module";
|
import { ProfileModule } from "./profile/profile.module";
|
||||||
import { SandHubModule } from "./sand-hub/sand-hub.module";
|
import { SandHubModule } from "./sand-hub/sand-hub.module";
|
||||||
|
import { SystemSettingsModule } from "./system-settings/system-settings.module";
|
||||||
import { ReportsModule } from "./reports/reports.module";
|
import { ReportsModule } from "./reports/reports.module";
|
||||||
import { RequestManagementModule } from "./request-management/request-management.module";
|
import { RequestManagementModule } from "./request-management/request-management.module";
|
||||||
import { UsersModule } from "./users/users.module";
|
import { UsersModule } from "./users/users.module";
|
||||||
@@ -58,6 +59,7 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
|||||||
PlatesModule,
|
PlatesModule,
|
||||||
RequestManagementModule,
|
RequestManagementModule,
|
||||||
SandHubModule,
|
SandHubModule,
|
||||||
|
SystemSettingsModule,
|
||||||
ExpertBlameModule,
|
ExpertBlameModule,
|
||||||
ClaimRequestManagementModule,
|
ClaimRequestManagementModule,
|
||||||
ExpertClaimModule,
|
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 { HttpModule } from "@nestjs/axios";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { MongooseModule } from "@nestjs/mongoose";
|
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 { SandHubDbService } from "src/sand-hub/entity/db-service/sand-hub.db.service";
|
||||||
import { SandHubModel, SandHubSchema } from "./entity/schema/sand-hub.schema";
|
import { SandHubModel, SandHubSchema } from "./entity/schema/sand-hub.schema";
|
||||||
import { SandHubService } from "./sand-hub.service";
|
import { SandHubService } from "./sand-hub.service";
|
||||||
@@ -8,6 +9,7 @@ import { SandHubService } from "./sand-hub.service";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
HttpModule,
|
HttpModule,
|
||||||
|
SystemSettingsModule,
|
||||||
MongooseModule.forFeature([
|
MongooseModule.forFeature([
|
||||||
{ name: SandHubModel.name, schema: SandHubSchema },
|
{ name: SandHubModel.name, schema: SandHubSchema },
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import axios from "axios";
|
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 { 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";
|
||||||
|
|
||||||
@@ -28,14 +29,15 @@ export class SandHubService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly sandHubDbService: SandHubDbService,
|
private readonly sandHubDbService: SandHubDbService,
|
||||||
|
private readonly systemSettingsService: SystemSettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When false (default), no outbound HTTP to SandHub/Tejarat gateways — all inquiries return mocks.
|
* When false, no outbound HTTP to SandHub/Tejarat — inquiries use permissive mocks.
|
||||||
* Set `SANDHUB_USE_REAL_API=true` to restore live calls.
|
* Controlled by `system_settings.externalApis.sandHubUseLiveApi` (PATCH /system-settings).
|
||||||
*/
|
*/
|
||||||
private useLiveSandHubApis(): boolean {
|
private async useLiveSandHubApis(): Promise<boolean> {
|
||||||
return process.env.SANDHUB_USE_REAL_API === "true";
|
return this.systemSettingsService.isSandHubLiveEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fixed plate/insurance inquiry payload used everywhere we mock block-inquiry style APIs. */
|
/** 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).
|
* 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();
|
const u = (url || "").toLowerCase();
|
||||||
if (u.includes("personal-inquiry")) {
|
if (u.includes("personal-inquiry")) {
|
||||||
|
const p = payload as { nationalCode?: string } | undefined;
|
||||||
|
const nin =
|
||||||
|
typeof p?.nationalCode === "string" ? p.nationalCode : "-";
|
||||||
return {
|
return {
|
||||||
message: "success",
|
message: "success",
|
||||||
data: {
|
data: {
|
||||||
@@ -127,7 +132,7 @@ export class SandHubService {
|
|||||||
lastName: "خانوادگی",
|
lastName: "خانوادگی",
|
||||||
fatherName: "-",
|
fatherName: "-",
|
||||||
birthCertificateNumber: "-",
|
birthCertificateNumber: "-",
|
||||||
nin: "-",
|
nin,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -165,7 +170,7 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getAccessToken(): Promise<string> {
|
private async getAccessToken(): Promise<string> {
|
||||||
if (!this.useLiveSandHubApis()) {
|
if (!(await this.useLiveSandHubApis())) {
|
||||||
return "mock-sandhub-access-token";
|
return "mock-sandhub-access-token";
|
||||||
}
|
}
|
||||||
if (this.loginToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
if (this.loginToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
||||||
@@ -205,7 +210,7 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getTejaratAccessToken(): Promise<string> {
|
private async getTejaratAccessToken(): Promise<string> {
|
||||||
if (!this.useLiveSandHubApis()) {
|
if (!(await this.useLiveSandHubApis())) {
|
||||||
return "mock-tejarat-access-token";
|
return "mock-tejarat-access-token";
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -263,7 +268,7 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async makeTejaratRequest(url: string, payload: any, maxRetries = 2) {
|
private async makeTejaratRequest(url: string, payload: any, maxRetries = 2) {
|
||||||
if (!this.useLiveSandHubApis()) {
|
if (!(await this.useLiveSandHubApis())) {
|
||||||
this.logger.log(`[MOCK] Tejarat POST skipped: ${url}`);
|
this.logger.log(`[MOCK] Tejarat POST skipped: ${url}`);
|
||||||
return this.getDefaultMockPlateInquiryRaw();
|
return this.getDefaultMockPlateInquiryRaw();
|
||||||
}
|
}
|
||||||
@@ -328,10 +333,11 @@ export class SandHubService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requestUrl = `${baseUrl}/block-inquiry-tejarat`;
|
const requestUrl = `${baseUrl}/block-inquiry-tejarat`;
|
||||||
const raw = this.useLiveSandHubApis()
|
const live = await this.useLiveSandHubApis();
|
||||||
|
const raw = live
|
||||||
? await this.makeTejaratRequest(requestUrl, requestPayload)
|
? await this.makeTejaratRequest(requestUrl, requestPayload)
|
||||||
: this.getDefaultMockPlateInquiryRaw();
|
: this.getDefaultMockPlateInquiryRaw();
|
||||||
if (!this.useLiveSandHubApis()) {
|
if (!live) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`[MOCK] getTejaratBlockInquiry plate=${JSON.stringify(requestPayload)}`,
|
`[MOCK] getTejaratBlockInquiry plate=${JSON.stringify(requestPayload)}`,
|
||||||
);
|
);
|
||||||
@@ -345,9 +351,9 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async makeSandHubRequest(url: string, payload: any, maxRetries = 3) {
|
private async makeSandHubRequest(url: string, payload: any, maxRetries = 3) {
|
||||||
if (!this.useLiveSandHubApis()) {
|
if (!(await this.useLiveSandHubApis())) {
|
||||||
this.logger.log(`[MOCK] SandHub POST skipped: ${url}`);
|
this.logger.log(`[MOCK] SandHub POST skipped: ${url}`);
|
||||||
return this.buildMockSandHubResponseForUrl(url);
|
return this.buildMockSandHubResponseForUrl(url, payload);
|
||||||
}
|
}
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const INITIAL_DELAY = 1000;
|
const INITIAL_DELAY = 1000;
|
||||||
@@ -453,7 +459,7 @@ export class SandHubService {
|
|||||||
const requestUrl = `${base}/block-inquiry-tejarat`;
|
const requestUrl = `${base}/block-inquiry-tejarat`;
|
||||||
|
|
||||||
let response: any;
|
let response: any;
|
||||||
if (this.useLiveSandHubApis()) {
|
if (await this.useLiveSandHubApis()) {
|
||||||
response = await this.makeSandHubRequest(requestUrl, requestPayload);
|
response = await this.makeSandHubRequest(requestUrl, requestPayload);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(
|
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