feat(tenants): store Khanegi deploy settings snapshot on tenants.setting
Import can_edit=0 settings via the sold version's admin login + get_settings, for Super Admin program-settings UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
-- Snapshot of Khanegi deploy settings (can_edit=0) per sold version
|
||||||
|
|
||||||
|
ALTER TABLE "tenants"
|
||||||
|
ADD COLUMN IF NOT EXISTS "setting" JSONB;
|
||||||
@@ -69,6 +69,8 @@ model Tenant {
|
|||||||
/// Legacy cache from older sync attempts — SuperAdmin SMS account is now source of truth
|
/// Legacy cache from older sync attempts — SuperAdmin SMS account is now source of truth
|
||||||
smsSettingsCache Json? @map("sms_settings_cache")
|
smsSettingsCache Json? @map("sms_settings_cache")
|
||||||
smsSyncedAt DateTime? @map("sms_synced_at")
|
smsSyncedAt DateTime? @map("sms_synced_at")
|
||||||
|
/// Khanegi deploy settings snapshot (can_edit=0) imported via admin login + get_settings.
|
||||||
|
setting Json?
|
||||||
/// Full Khanegi UI JSON (theme, navigation, pages). Hamkar: unused for now.
|
/// Full Khanegi UI JSON (theme, navigation, pages). Hamkar: unused for now.
|
||||||
appUiConfig Json? @map("app_ui_config")
|
appUiConfig Json? @map("app_ui_config")
|
||||||
/// Khanegi only: which appearance editor / get_user payload shape to use.
|
/// Khanegi only: which appearance editor / get_user payload shape to use.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { GetUserModule } from './get-user/get-user.module';
|
|||||||
import { GetVersionModule } from './get-version/get-version.module';
|
import { GetVersionModule } from './get-version/get-version.module';
|
||||||
import { GetManifestModule } from './get-manifest/get-manifest.module';
|
import { GetManifestModule } from './get-manifest/get-manifest.module';
|
||||||
import { SettingsProxyModule } from './settings-proxy/settings-proxy.module';
|
import { SettingsProxyModule } from './settings-proxy/settings-proxy.module';
|
||||||
|
import { ProgramSettingsModule } from './program-settings/program-settings.module';
|
||||||
import { AuditModule } from './audit/audit.module';
|
import { AuditModule } from './audit/audit.module';
|
||||||
import { SmsModule } from './sms/sms.module';
|
import { SmsModule } from './sms/sms.module';
|
||||||
import { ServersModule } from './servers/servers.module';
|
import { ServersModule } from './servers/servers.module';
|
||||||
@@ -27,6 +28,7 @@ import { HealthController } from './health.controller';
|
|||||||
GetVersionModule,
|
GetVersionModule,
|
||||||
GetManifestModule,
|
GetManifestModule,
|
||||||
SettingsProxyModule,
|
SettingsProxyModule,
|
||||||
|
ProgramSettingsModule,
|
||||||
AuditModule,
|
AuditModule,
|
||||||
SmsModule,
|
SmsModule,
|
||||||
ServersModule,
|
ServersModule,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class ImportProgramSettingsDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3)
|
||||||
|
@MaxLength(32)
|
||||||
|
mobile!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(120)
|
||||||
|
password!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||||
|
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||||
|
import { CurrentUser } from '../auth/current-user.decorator';
|
||||||
|
import { ImportProgramSettingsDto } from './dto/import-program-settings.dto';
|
||||||
|
import { ProgramSettingsService } from './program-settings.service';
|
||||||
|
|
||||||
|
@Controller('tenants/:tenantId/program-settings')
|
||||||
|
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||||
|
export class ProgramSettingsController {
|
||||||
|
constructor(private readonly programSettings: ProgramSettingsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@RequirePermissions('khanegi:read')
|
||||||
|
get(@Param('tenantId') tenantId: string) {
|
||||||
|
return this.programSettings.get(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('import')
|
||||||
|
@RequirePermissions('khanegi:write')
|
||||||
|
import(
|
||||||
|
@Param('tenantId') tenantId: string,
|
||||||
|
@Body() body: ImportProgramSettingsDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
) {
|
||||||
|
return this.programSettings.importFromKhanegi(
|
||||||
|
tenantId,
|
||||||
|
body.mobile,
|
||||||
|
body.password,
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { ProgramSettingsController } from './program-settings.controller';
|
||||||
|
import { ProgramSettingsService } from './program-settings.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AuthModule],
|
||||||
|
controllers: [ProgramSettingsController],
|
||||||
|
providers: [ProgramSettingsService],
|
||||||
|
exports: [ProgramSettingsService],
|
||||||
|
})
|
||||||
|
export class ProgramSettingsModule {}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
import {
|
||||||
|
BadGatewayException,
|
||||||
|
BadRequestException,
|
||||||
|
ForbiddenException,
|
||||||
|
HttpException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Prisma, ProductType } from '@prisma/client';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { AuditService } from '../audit/audit.service';
|
||||||
|
|
||||||
|
export type ProgramSettingRow = {
|
||||||
|
id?: number;
|
||||||
|
key?: string;
|
||||||
|
value?: string | number | boolean | null;
|
||||||
|
title_fa?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
type?: string | null;
|
||||||
|
is_active?: number | boolean | string | null;
|
||||||
|
can_edit?: number | boolean | string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProgramSettingsPayload = {
|
||||||
|
importedAt: string;
|
||||||
|
source: 'khanegi_admin';
|
||||||
|
apiBase: string;
|
||||||
|
items: ProgramSettingRow[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProgramSettingsService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly audit: AuditService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async get(tenantId: string) {
|
||||||
|
const tenant = await this.requireKhanegiTenant(tenantId);
|
||||||
|
const stored = this.readStored(tenant.setting);
|
||||||
|
return {
|
||||||
|
tenant: this.tenantMeta(tenant),
|
||||||
|
importedAt: stored?.importedAt ?? null,
|
||||||
|
source: stored?.source ?? null,
|
||||||
|
apiBase: stored?.apiBase ?? null,
|
||||||
|
items: stored?.items ?? [],
|
||||||
|
count: stored?.items?.length ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async importFromKhanegi(
|
||||||
|
tenantId: string,
|
||||||
|
mobile: string,
|
||||||
|
password: string,
|
||||||
|
userId?: string,
|
||||||
|
) {
|
||||||
|
const tenant = await this.requireKhanegiTenant(tenantId);
|
||||||
|
const mobileNorm = this.normalizeAdminMobile(mobile);
|
||||||
|
if (!/^\d{11}$/.test(mobileNorm)) {
|
||||||
|
throw new BadRequestException('موبایل ادمین خانگی باید ۱۱ رقم باشد (مثلاً 0912…)');
|
||||||
|
}
|
||||||
|
const { apiBase, token } = await this.loginKhanegiAdmin(
|
||||||
|
tenant,
|
||||||
|
mobileNorm,
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
const items = await this.fetchSystemSettings(apiBase, token);
|
||||||
|
const payload: ProgramSettingsPayload = {
|
||||||
|
importedAt: new Date().toISOString(),
|
||||||
|
source: 'khanegi_admin',
|
||||||
|
apiBase,
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.prisma.tenant.update({
|
||||||
|
where: { id: tenantId },
|
||||||
|
data: { setting: payload as unknown as Prisma.InputJsonValue },
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.audit.log({
|
||||||
|
action: 'tenant.program_settings.import',
|
||||||
|
entityType: 'tenant_program_settings',
|
||||||
|
entityId: tenantId,
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
after: {
|
||||||
|
importedAt: payload.importedAt,
|
||||||
|
count: items.length,
|
||||||
|
keys: items.map((row) => row.key).filter(Boolean),
|
||||||
|
} as Prisma.InputJsonValue,
|
||||||
|
meta: { apiBase } as Prisma.InputJsonValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenant: this.tenantMeta(tenant),
|
||||||
|
importedAt: payload.importedAt,
|
||||||
|
source: payload.source,
|
||||||
|
apiBase,
|
||||||
|
items,
|
||||||
|
count: items.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requireKhanegiTenant(tenantId: string) {
|
||||||
|
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId } });
|
||||||
|
if (!tenant) {
|
||||||
|
throw new NotFoundException('نسخه یافت نشد');
|
||||||
|
}
|
||||||
|
if (tenant.product !== ProductType.khanegi) {
|
||||||
|
throw new ForbiddenException('تنظیمات برنامه فعلاً فقط برای خانگی فعال است');
|
||||||
|
}
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
|
private tenantMeta(tenant: {
|
||||||
|
id: string;
|
||||||
|
nameFa: string;
|
||||||
|
slug: string;
|
||||||
|
domain: string;
|
||||||
|
product: ProductType;
|
||||||
|
appUrl: string | null;
|
||||||
|
adminUrl: string | null;
|
||||||
|
uiVariant: string;
|
||||||
|
nodeServer: string;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: tenant.id,
|
||||||
|
nameFa: tenant.nameFa,
|
||||||
|
slug: tenant.slug,
|
||||||
|
domain: tenant.domain,
|
||||||
|
product: tenant.product,
|
||||||
|
appUrl: tenant.appUrl,
|
||||||
|
adminUrl: tenant.adminUrl,
|
||||||
|
uiVariant: tenant.uiVariant,
|
||||||
|
nodeServer: tenant.nodeServer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private readStored(raw: Prisma.JsonValue | null): ProgramSettingsPayload | null {
|
||||||
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return {
|
||||||
|
importedAt: '',
|
||||||
|
source: 'khanegi_admin',
|
||||||
|
apiBase: '',
|
||||||
|
items: raw.filter((row) => this.isSystemSetting(row as ProgramSettingRow)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const obj = raw as Record<string, unknown>;
|
||||||
|
const items = this.asSettingsArray(obj.items ?? obj);
|
||||||
|
return {
|
||||||
|
importedAt: String(obj.importedAt || ''),
|
||||||
|
source: 'khanegi_admin',
|
||||||
|
apiBase: String(obj.apiBase || ''),
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loginKhanegiAdmin(
|
||||||
|
tenant: {
|
||||||
|
nodeServer: string;
|
||||||
|
domain: string;
|
||||||
|
},
|
||||||
|
mobile: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<{ apiBase: string; token: string }> {
|
||||||
|
const candidates = this.apiBaseCandidates(tenant);
|
||||||
|
if (!candidates.length) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'آدرس API نسخه (node_server) تنظیم نشده؛ اول در مدیریت نسخه ذخیره کنید.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError: unknown;
|
||||||
|
for (const apiBase of candidates) {
|
||||||
|
try {
|
||||||
|
const res = await axios.post(
|
||||||
|
this.joinUrl(apiBase, '/auth/login'),
|
||||||
|
{ mobile: mobile.trim(), password, provider: 'user' },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 20000,
|
||||||
|
validateStatus: () => true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 404 || res.status === 405) {
|
||||||
|
lastError = new BadGatewayException(`ورود روی ${apiBase} یافت نشد`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (res.status >= 500) {
|
||||||
|
lastError = new BadGatewayException(
|
||||||
|
this.laravelMessage(res.data) || `سرور خانگی ${apiBase} خطا داد`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (res.status === 401 || res.status === 422 || res.status === 400) {
|
||||||
|
throw new BadGatewayException(
|
||||||
|
this.laravelMessage(res.data) ||
|
||||||
|
'نام کاربری یا رمز عبور ادمین خانگی اشتباه است',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.extractAccessToken(res.data);
|
||||||
|
if (token) {
|
||||||
|
return { apiBase, token };
|
||||||
|
}
|
||||||
|
|
||||||
|
lastError = new BadGatewayException(
|
||||||
|
this.laravelMessage(res.data) ||
|
||||||
|
`ورود به ادمین خانگی از ${apiBase} توکن برنگرداند`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
lastError = error;
|
||||||
|
const status = axios.isAxiosError(error) ? error.response?.status : undefined;
|
||||||
|
if (status === 401 || status === 422 || status === 400) {
|
||||||
|
throw new BadGatewayException(
|
||||||
|
this.axiosMessage(error, 'نام کاربری یا رمز عبور ادمین خانگی اشتباه است'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadGatewayException(
|
||||||
|
this.axiosMessage(
|
||||||
|
lastError,
|
||||||
|
'ورود به API ادمین خانگی ناموفق بود — node_server و دامنه نسخه را چک کنید',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchSystemSettings(apiBase: string, token: string) {
|
||||||
|
const url = this.joinUrl(apiBase, '/admin/get_settings');
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(url, {
|
||||||
|
params: { system_only: 1 },
|
||||||
|
headers,
|
||||||
|
timeout: 25000,
|
||||||
|
});
|
||||||
|
const items = this.asSettingsArray(data).filter((row) => this.isSystemSetting(row));
|
||||||
|
if (items.length) return items;
|
||||||
|
} catch (error) {
|
||||||
|
const status = axios.isAxiosError(error) ? error.response?.status : undefined;
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
throw new BadGatewayException(
|
||||||
|
this.axiosMessage(error, 'توکن ادمین خانگی برای get_settings رد شد'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(url, { headers, timeout: 25000 });
|
||||||
|
return this.asSettingsArray(data).filter((row) => this.isSystemSetting(row));
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadGatewayException(
|
||||||
|
this.axiosMessage(error, 'خواندن get_settings از اپ خانگی ناموفق بود'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeAdminMobile(mobile: string): string {
|
||||||
|
let digits = String(mobile || '').replace(/\D+/g, '');
|
||||||
|
if (digits.startsWith('98') && digits.length >= 12) {
|
||||||
|
digits = digits.slice(2);
|
||||||
|
}
|
||||||
|
if (digits.length === 10 && digits.startsWith('9')) {
|
||||||
|
digits = `0${digits}`;
|
||||||
|
}
|
||||||
|
return digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
private apiBaseCandidates(tenant: { nodeServer: string; domain: string }): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const push = (value?: string | null) => {
|
||||||
|
const trimmed = String(value || '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\/$/, '');
|
||||||
|
if (trimmed && !out.includes(trimmed)) out.push(trimmed);
|
||||||
|
};
|
||||||
|
|
||||||
|
push(tenant.nodeServer);
|
||||||
|
const host = this.normalizeHost(tenant.domain);
|
||||||
|
if (host) {
|
||||||
|
push(`https://${host}/server`);
|
||||||
|
push(`https://${host}/api`);
|
||||||
|
push(`https://${host}`);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractAccessToken(data: unknown): string {
|
||||||
|
if (!data || typeof data !== 'object') return '';
|
||||||
|
const obj = data as Record<string, unknown>;
|
||||||
|
const nested =
|
||||||
|
obj.data && typeof obj.data === 'object'
|
||||||
|
? (obj.data as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const raw = obj.access_token ?? nested?.access_token ?? obj.token ?? nested?.token;
|
||||||
|
return String(raw || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private asSettingsArray(data: unknown): ProgramSettingRow[] {
|
||||||
|
if (Array.isArray(data)) return data as ProgramSettingRow[];
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
const obj = data as Record<string, unknown>;
|
||||||
|
if (Array.isArray(obj.data)) return obj.data as ProgramSettingRow[];
|
||||||
|
if (Array.isArray(obj.settings)) return obj.settings as ProgramSettingRow[];
|
||||||
|
if (Array.isArray(obj.items)) return obj.items as ProgramSettingRow[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSystemSetting(row: ProgramSettingRow): boolean {
|
||||||
|
if (!this.isLocked(row?.can_edit)) return false;
|
||||||
|
return this.isActive(row?.is_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLocked(canEdit: ProgramSettingRow['can_edit']): boolean {
|
||||||
|
return canEdit === 0 || canEdit === false || canEdit === '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private isActive(isActive: ProgramSettingRow['is_active']): boolean {
|
||||||
|
if (isActive === 0 || isActive === false || isActive === '0') return false;
|
||||||
|
return isActive !== null && isActive !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeHost(value?: string | null) {
|
||||||
|
if (!value) return '';
|
||||||
|
return String(value)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^https?:\/\//, '')
|
||||||
|
.split('/')[0]
|
||||||
|
.split(':')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinUrl(base: string, path: string) {
|
||||||
|
return `${base.replace(/\/$/, '')}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private laravelMessage(data: unknown): string {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return typeof data === 'string' ? data : '';
|
||||||
|
}
|
||||||
|
const obj = data as Record<string, unknown>;
|
||||||
|
if (typeof obj.message === 'string' && obj.message.trim()) return obj.message;
|
||||||
|
if (Array.isArray(obj.message)) return obj.message.map(String).join('، ');
|
||||||
|
if (obj.errors && typeof obj.errors === 'object') {
|
||||||
|
const parts = Object.values(obj.errors as Record<string, unknown>)
|
||||||
|
.flat()
|
||||||
|
.map(String)
|
||||||
|
.filter(Boolean);
|
||||||
|
if (parts.length) return parts.join('، ');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private axiosMessage(error: unknown, fallback: string) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const status = error.response?.status;
|
||||||
|
const body = error.response?.data;
|
||||||
|
const fromBody = this.laravelMessage(body);
|
||||||
|
if (fromBody) {
|
||||||
|
return `${fallback}${status ? ` (HTTP ${status})` : ''}: ${fromBody}`;
|
||||||
|
}
|
||||||
|
return `${fallback}${status ? ` (HTTP ${status})` : ''}`;
|
||||||
|
}
|
||||||
|
if (error instanceof Error && error.message) {
|
||||||
|
return `${fallback}: ${error.message}`;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ export class TenantsService {
|
|||||||
appUrl: true,
|
appUrl: true,
|
||||||
adminUrl: true,
|
adminUrl: true,
|
||||||
appUiConfig: true,
|
appUiConfig: true,
|
||||||
|
setting: true,
|
||||||
uiVariant: true,
|
uiVariant: true,
|
||||||
appVersion: true,
|
appVersion: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -47,9 +48,10 @@ export class TenantsService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return rows.map(({ appUiConfig, ...rest }) => ({
|
return rows.map(({ appUiConfig, setting, ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
hasAppUiConfig: Boolean(appUiConfig),
|
hasAppUiConfig: Boolean(appUiConfig),
|
||||||
|
hasProgramSettings: Boolean(setting),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,10 +236,11 @@ export class TenantsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sanitize(tenant: Tenant) {
|
private sanitize(tenant: Tenant) {
|
||||||
const { remoteApiToken, ...rest } = tenant;
|
const { remoteApiToken, setting, ...rest } = tenant;
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
hasRemoteApiToken: Boolean(remoteApiToken),
|
hasRemoteApiToken: Boolean(remoteApiToken),
|
||||||
|
hasProgramSettings: Boolean(setting),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user