fix(plugins): per-tenant plugin store flag and harden broken schema APIs
Store enablement is now a tenant settings row instead of a global runtime switch, and expiring/requests endpoints degrade to empty lists when store tables are missing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,8 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { NotificationsModule } from '../notifications/notifications.module';
|
|
||||||
import { GetUserController } from './get-user.controller';
|
import { GetUserController } from './get-user.controller';
|
||||||
import { GetUserService } from './get-user.service';
|
import { GetUserService } from './get-user.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [NotificationsModule],
|
|
||||||
controllers: [GetUserController],
|
controllers: [GetUserController],
|
||||||
providers: [GetUserService],
|
providers: [GetUserService],
|
||||||
exports: [GetUserService],
|
exports: [GetUserService],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { iranDaysUntil } from '../common/iran-time.util';
|
|||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { applyKhanegiNavSample } from './khanegi-nav-sample';
|
import { applyKhanegiNavSample } from './khanegi-nav-sample';
|
||||||
import { rewriteLegacyUploadUrl } from '../uploads/uploads.util';
|
import { rewriteLegacyUploadUrl } from '../uploads/uploads.util';
|
||||||
import { RuntimeSettingsService } from '../notifications/runtime-settings.service';
|
import { PLUGIN_STORE_SETTING_KEY } from '../program-settings/runtime-setting-keys';
|
||||||
|
|
||||||
type UiSetting = { key: string; value: string };
|
type UiSetting = { key: string; value: string };
|
||||||
|
|
||||||
@@ -25,10 +25,7 @@ type SubscriptionAlerts = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetUserService {
|
export class GetUserService {
|
||||||
constructor(
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly runtime: RuntimeSettingsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async resolveByHost(hostHeader?: string, domainQuery?: string) {
|
async resolveByHost(hostHeader?: string, domainQuery?: string) {
|
||||||
const host = this.normalizeHost(domainQuery || hostHeader || '');
|
const host = this.normalizeHost(domainQuery || hostHeader || '');
|
||||||
@@ -116,7 +113,9 @@ export class GetUserService {
|
|||||||
ui_settings: uiSettings.length ? uiSettings : undefined,
|
ui_settings: uiSettings.length ? uiSettings : undefined,
|
||||||
/** Admin expiry modals — near/expired server, plugin, support */
|
/** Admin expiry modals — near/expired server, plugin, support */
|
||||||
subscription_alerts,
|
subscription_alerts,
|
||||||
plugin_store_enabled: isKhanegi ? this.runtime.getPluginStoreEnabled() : false,
|
plugin_store_enabled: isKhanegi
|
||||||
|
? await this.isPluginStoreEnabled(tenant.id)
|
||||||
|
: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -125,6 +124,18 @@ export class GetUserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async isPluginStoreEnabled(tenantId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const row = await this.prisma.setting.findUnique({
|
||||||
|
where: { tenantId_key: { tenantId, key: PLUGIN_STORE_SETTING_KEY } },
|
||||||
|
select: { value: true },
|
||||||
|
});
|
||||||
|
return row?.value === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async buildSubscriptionAlerts(tenantId: string): Promise<SubscriptionAlerts> {
|
private async buildSubscriptionAlerts(tenantId: string): Promise<SubscriptionAlerts> {
|
||||||
const [servers, plugins, supports] = await Promise.all([
|
const [servers, plugins, supports] = await Promise.all([
|
||||||
this.prisma.serverAccount.findMany({
|
this.prisma.serverAccount.findMany({
|
||||||
|
|||||||
@@ -22,17 +22,4 @@ export class RuntimeSettingsController {
|
|||||||
this.settings.setAdminSmsEnabled(dto.enabled);
|
this.settings.setAdminSmsEnabled(dto.enabled);
|
||||||
return this.settings.getPublic();
|
return this.settings.getPublic();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('plugin-store')
|
|
||||||
@RequirePermissions('settings:write')
|
|
||||||
getPluginStore() {
|
|
||||||
return this.settings.getPublic();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch('plugin-store')
|
|
||||||
@RequirePermissions('settings:write')
|
|
||||||
setPluginStore(@Body() dto: UpdateAdminSmsDto) {
|
|
||||||
this.settings.setPluginStoreEnabled(dto.enabled);
|
|
||||||
return this.settings.getPublic();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,10 @@ import { join } from 'path';
|
|||||||
type RuntimeSettings = {
|
type RuntimeSettings = {
|
||||||
/** When false, SuperAdmin stops SMS alerts to managers */
|
/** When false, SuperAdmin stops SMS alerts to managers */
|
||||||
adminSmsEnabled: boolean;
|
adminSmsEnabled: boolean;
|
||||||
/** When false, khanegi admin plugin store menu is hidden */
|
|
||||||
pluginStoreEnabled: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULTS: RuntimeSettings = {
|
const DEFAULTS: RuntimeSettings = {
|
||||||
adminSmsEnabled: true,
|
adminSmsEnabled: true,
|
||||||
pluginStoreEnabled: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -31,22 +28,13 @@ export class RuntimeSettingsService {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPluginStoreEnabled(): boolean {
|
|
||||||
return this.read().pluginStoreEnabled === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPluginStoreEnabled(enabled: boolean) {
|
|
||||||
const next = { ...this.read(), pluginStoreEnabled: Boolean(enabled) };
|
|
||||||
this.write(next);
|
|
||||||
this.logger.log(`Plugin store ${next.pluginStoreEnabled ? 'enabled' : 'disabled'}`);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPublic() {
|
getPublic() {
|
||||||
return {
|
try {
|
||||||
adminSmsEnabled: this.getAdminSmsEnabled(),
|
return { adminSmsEnabled: this.getAdminSmsEnabled() };
|
||||||
pluginStoreEnabled: this.getPluginStoreEnabled(),
|
} catch (error: any) {
|
||||||
};
|
this.logger.warn(`getPublic failed: ${error?.message || error}`);
|
||||||
|
return { adminSmsEnabled: true };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private read(): RuntimeSettings {
|
private read(): RuntimeSettings {
|
||||||
@@ -59,7 +47,6 @@ export class RuntimeSettingsService {
|
|||||||
const raw = JSON.parse(readFileSync(this.filePath, 'utf8')) as Partial<RuntimeSettings>;
|
const raw = JSON.parse(readFileSync(this.filePath, 'utf8')) as Partial<RuntimeSettings>;
|
||||||
this.cache = {
|
this.cache = {
|
||||||
adminSmsEnabled: raw.adminSmsEnabled !== false,
|
adminSmsEnabled: raw.adminSmsEnabled !== false,
|
||||||
pluginStoreEnabled: raw.pluginStoreEnabled === true,
|
|
||||||
};
|
};
|
||||||
return this.cache;
|
return this.cache;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -137,3 +137,8 @@ export class RenewPluginDto {
|
|||||||
@MaxLength(500)
|
@MaxLength(500)
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdatePluginStoreEnabledDto {
|
||||||
|
@IsBoolean()
|
||||||
|
enabled!: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import { PrismaService } from '../prisma/prisma.service';
|
|||||||
import { AuditService } from '../audit/audit.service';
|
import { AuditService } from '../audit/audit.service';
|
||||||
import { AdminNotifyService } from '../notifications/admin-notify.service';
|
import { AdminNotifyService } from '../notifications/admin-notify.service';
|
||||||
import { CentralSmsService } from '../notifications/central-sms.service';
|
import { CentralSmsService } from '../notifications/central-sms.service';
|
||||||
import { RuntimeSettingsService } from '../notifications/runtime-settings.service';
|
|
||||||
import { ProgramSettingsService } from '../program-settings/program-settings.service';
|
import { ProgramSettingsService } from '../program-settings/program-settings.service';
|
||||||
|
import { PLUGIN_STORE_SETTING_KEY } from '../program-settings/runtime-setting-keys';
|
||||||
import {
|
import {
|
||||||
addIranMonths,
|
addIranMonths,
|
||||||
addIranPlanExpiry,
|
addIranPlanExpiry,
|
||||||
@@ -55,7 +55,6 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
private readonly audit: AuditService,
|
private readonly audit: AuditService,
|
||||||
private readonly notify: AdminNotifyService,
|
private readonly notify: AdminNotifyService,
|
||||||
private readonly sms: CentralSmsService,
|
private readonly sms: CentralSmsService,
|
||||||
private readonly runtime: RuntimeSettingsService,
|
|
||||||
private readonly programSettings: ProgramSettingsService,
|
private readonly programSettings: ProgramSettingsService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
@@ -135,6 +134,69 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return inferSettingKeyFromName(name);
|
return inferSettingKeyFromName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isStoreEnabled(tenantId: string): Promise<boolean> {
|
||||||
|
if (!tenantId) return false;
|
||||||
|
try {
|
||||||
|
const row = await this.prisma.setting.findUnique({
|
||||||
|
where: { tenantId_key: { tenantId, key: PLUGIN_STORE_SETTING_KEY } },
|
||||||
|
select: { value: true },
|
||||||
|
});
|
||||||
|
return row?.value === 'true';
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`isStoreEnabled ${tenantId}: ${error?.message || error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeEnabledMap(tenantIds: string[]): Promise<Map<string, boolean>> {
|
||||||
|
const map = new Map<string, boolean>();
|
||||||
|
if (!tenantIds.length) return map;
|
||||||
|
try {
|
||||||
|
const rows = await this.prisma.setting.findMany({
|
||||||
|
where: { key: PLUGIN_STORE_SETTING_KEY, tenantId: { in: tenantIds } },
|
||||||
|
select: { tenantId: true, value: true },
|
||||||
|
});
|
||||||
|
for (const row of rows) {
|
||||||
|
map.set(row.tenantId, row.value === 'true');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`storeEnabledMap: ${error?.message || error}`);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setStoreEnabled(tenantId: string, enabled: boolean, userId?: string) {
|
||||||
|
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId } });
|
||||||
|
if (!tenant) throw new NotFoundException('نسخه یافت نشد');
|
||||||
|
if (tenant.product !== ProductType.khanegi) {
|
||||||
|
throw new BadRequestException('فروشگاه افزونه فقط برای خانگی است');
|
||||||
|
}
|
||||||
|
const value = enabled ? 'true' : 'false';
|
||||||
|
await this.prisma.setting.upsert({
|
||||||
|
where: { tenantId_key: { tenantId, key: PLUGIN_STORE_SETTING_KEY } },
|
||||||
|
create: {
|
||||||
|
tenantId,
|
||||||
|
key: PLUGIN_STORE_SETTING_KEY,
|
||||||
|
titleFa: 'فروشگاه افزونه',
|
||||||
|
value,
|
||||||
|
type: 'boolean',
|
||||||
|
canEdit: 0,
|
||||||
|
isExist: true,
|
||||||
|
isActive: 1,
|
||||||
|
},
|
||||||
|
update: { value, deletedAt: null },
|
||||||
|
});
|
||||||
|
await this.audit.log({
|
||||||
|
action: 'plugin_store.toggle',
|
||||||
|
entityType: 'tenant',
|
||||||
|
entityId: tenantId,
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
after: { plugin_store_enabled: enabled } as Prisma.InputJsonValue,
|
||||||
|
});
|
||||||
|
return { pluginStoreEnabled: enabled };
|
||||||
|
}
|
||||||
|
|
||||||
async applySettingSwitch(tenantId: string, settingKey: string, on: boolean) {
|
async applySettingSwitch(tenantId: string, settingKey: string, on: boolean) {
|
||||||
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId } });
|
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId } });
|
||||||
if (!tenant || tenant.product !== ProductType.khanegi) return;
|
if (!tenant || tenant.product !== ProductType.khanegi) return;
|
||||||
@@ -148,12 +210,17 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listCatalogAdmin() {
|
async listCatalogAdmin() {
|
||||||
|
try {
|
||||||
await this.seedCatalog();
|
await this.seedCatalog();
|
||||||
const items = await this.prisma.pluginCatalogItem.findMany({
|
const items = await this.prisma.pluginCatalogItem.findMany({
|
||||||
orderBy: { titleFa: 'asc' },
|
orderBy: { titleFa: 'asc' },
|
||||||
include: { prices: { orderBy: { durationDays: 'asc' } } },
|
include: { prices: { orderBy: { durationDays: 'asc' } } },
|
||||||
});
|
});
|
||||||
return items.map((item) => this.toCatalogAdmin(item));
|
return items.map((item) => this.toCatalogAdmin(item));
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`listCatalogAdmin skipped: ${error?.message || error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveCatalogItem(id: string, dto: SaveCatalogItemDto, userId?: string) {
|
async saveCatalogItem(id: string, dto: SaveCatalogItemDto, userId?: string) {
|
||||||
@@ -254,6 +321,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listRequestsAdmin(status?: string) {
|
async listRequestsAdmin(status?: string) {
|
||||||
|
try {
|
||||||
const where: Prisma.PluginPurchaseRequestWhereInput = {
|
const where: Prisma.PluginPurchaseRequestWhereInput = {
|
||||||
tenant: { product: ProductType.khanegi },
|
tenant: { product: ProductType.khanegi },
|
||||||
};
|
};
|
||||||
@@ -271,6 +339,10 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return rows.map((row) => this.toRequestRow(row));
|
return rows.map((row) => this.toRequestRow(row));
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`listRequestsAdmin skipped: ${error?.message || error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async approveRequest(id: string, dto: ApprovePurchaseDto, userId?: string) {
|
async approveRequest(id: string, dto: ApprovePurchaseDto, userId?: string) {
|
||||||
@@ -396,12 +468,8 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteStoreEnabled() {
|
|
||||||
return this.runtime.getPluginStoreEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
async remoteStore(tenantId: string, product: string) {
|
async remoteStore(tenantId: string, product: string) {
|
||||||
this.assertRemoteKhanegi(tenantId, product);
|
await this.assertRemoteKhanegi(tenantId, product);
|
||||||
const items = await this.prisma.pluginCatalogItem.findMany({
|
const items = await this.prisma.pluginCatalogItem.findMany({
|
||||||
where: { inStore: true },
|
where: { inStore: true },
|
||||||
orderBy: { titleFa: 'asc' },
|
orderBy: { titleFa: 'asc' },
|
||||||
@@ -462,7 +530,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remoteListRequests(tenantId: string, product: string) {
|
async remoteListRequests(tenantId: string, product: string) {
|
||||||
this.assertRemoteKhanegi(tenantId, product);
|
await this.assertRemoteKhanegi(tenantId, product);
|
||||||
const rows = await this.prisma.pluginPurchaseRequest.findMany({
|
const rows = await this.prisma.pluginPurchaseRequest.findMany({
|
||||||
where: { tenantId },
|
where: { tenantId },
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
@@ -479,7 +547,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
product: string,
|
product: string,
|
||||||
dto: CreatePurchaseRequestDto,
|
dto: CreatePurchaseRequestDto,
|
||||||
) {
|
) {
|
||||||
this.assertRemoteKhanegi(tenantId, product);
|
await this.assertRemoteKhanegi(tenantId, product);
|
||||||
const item = await this.prisma.pluginCatalogItem.findUnique({
|
const item = await this.prisma.pluginCatalogItem.findUnique({
|
||||||
where: { id: dto.catalogItemId },
|
where: { id: dto.catalogItemId },
|
||||||
include: { prices: true },
|
include: { prices: true },
|
||||||
@@ -541,7 +609,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remoteCancelRequest(tenantId: string, product: string, requestId: string) {
|
async remoteCancelRequest(tenantId: string, product: string, requestId: string) {
|
||||||
this.assertRemoteKhanegi(tenantId, product);
|
await this.assertRemoteKhanegi(tenantId, product);
|
||||||
const req = await this.prisma.pluginPurchaseRequest.findFirst({
|
const req = await this.prisma.pluginPurchaseRequest.findFirst({
|
||||||
where: { id: requestId, tenantId },
|
where: { id: requestId, tenantId },
|
||||||
});
|
});
|
||||||
@@ -622,14 +690,14 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertRemoteKhanegi(tenantId: string, product: string) {
|
private async assertRemoteKhanegi(tenantId: string, product: string) {
|
||||||
if (product !== 'khanegi') {
|
if (product !== 'khanegi') {
|
||||||
throw new ForbiddenException('فروشگاه افزونه فقط برای خانگی است');
|
throw new ForbiddenException('فروشگاه افزونه فقط برای خانگی است');
|
||||||
}
|
}
|
||||||
if (!this.runtime.getPluginStoreEnabled()) {
|
|
||||||
throw new ForbiddenException('فروشگاه افزونه خاموش است');
|
|
||||||
}
|
|
||||||
if (!tenantId) throw new ForbiddenException('نسخه نامعتبر است');
|
if (!tenantId) throw new ForbiddenException('نسخه نامعتبر است');
|
||||||
|
if (!(await this.isStoreEnabled(tenantId))) {
|
||||||
|
throw new ForbiddenException('فروشگاه افزونه برای این نسخه خاموش است');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertPrices(prices: SaveCatalogItemDto['prices']) {
|
private assertPrices(prices: SaveCatalogItemDto['prices']) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
CreatePluginDto,
|
CreatePluginDto,
|
||||||
RenewPluginDto,
|
RenewPluginDto,
|
||||||
UpdatePluginDto,
|
UpdatePluginDto,
|
||||||
|
UpdatePluginStoreEnabledDto,
|
||||||
} from './dto/plugin.dto';
|
} from './dto/plugin.dto';
|
||||||
import { PluginsService } from './plugins.service';
|
import { PluginsService } from './plugins.service';
|
||||||
|
|
||||||
@@ -37,6 +38,16 @@ export class PluginsController {
|
|||||||
return this.pluginsService.getExpiringSoon(Number(limit) || 5);
|
return this.pluginsService.getExpiringSoon(Number(limit) || 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('tenants/:tenantId/store-enabled')
|
||||||
|
@RequirePermissions('plugins:write')
|
||||||
|
setStoreEnabled(
|
||||||
|
@Param('tenantId') tenantId: string,
|
||||||
|
@Body() dto: UpdatePluginStoreEnabledDto,
|
||||||
|
@CurrentUser() user: { id: string },
|
||||||
|
) {
|
||||||
|
return this.pluginsService.setStoreEnabled(tenantId, dto.enabled, user.id);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('tenants/:tenantId')
|
@Get('tenants/:tenantId')
|
||||||
@RequirePermissions('plugins:read')
|
@RequirePermissions('plugins:read')
|
||||||
getTenantPlugins(@Param('tenantId') tenantId: string) {
|
getTenantPlugins(@Param('tenantId') tenantId: string) {
|
||||||
|
|||||||
+122
-27
@@ -53,19 +53,11 @@ export class PluginsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
async listTenants() {
|
async listTenants() {
|
||||||
void this.runExpiryAlerts();
|
void this.runExpiryAlerts();
|
||||||
|
|
||||||
const rows = await this.prisma.tenant.findMany({
|
const rows = await this.loadTenantRows();
|
||||||
where: { isActive: true },
|
const flags = await this.store.storeEnabledMap(rows.map((row) => row.id));
|
||||||
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
|
||||||
include: {
|
|
||||||
pluginSubscriptions: {
|
|
||||||
where: { isActive: true },
|
|
||||||
orderBy: { name: 'asc' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows.map((tenant) => {
|
return rows.map((tenant) => {
|
||||||
const plugins = tenant.pluginSubscriptions.map((p) => this.toSummary(p));
|
const plugins = (tenant.pluginSubscriptions || []).map((p) => this.toSummary(p));
|
||||||
const worst = this.worstLevel(plugins.map((p) => p.expiryLevel));
|
const worst = this.worstLevel(plugins.map((p) => p.expiryLevel));
|
||||||
return {
|
return {
|
||||||
id: tenant.id,
|
id: tenant.id,
|
||||||
@@ -75,6 +67,8 @@ export class PluginsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
nameEn: tenant.nameEn,
|
nameEn: tenant.nameEn,
|
||||||
domain: tenant.domain,
|
domain: tenant.domain,
|
||||||
avatarUrl: tenant.avatarUrl,
|
avatarUrl: tenant.avatarUrl,
|
||||||
|
pluginStoreEnabled:
|
||||||
|
tenant.product === 'khanegi' && flags.get(tenant.id) === true,
|
||||||
pluginCount: plugins.length,
|
pluginCount: plugins.length,
|
||||||
plugins,
|
plugins,
|
||||||
summary: {
|
summary: {
|
||||||
@@ -96,13 +90,28 @@ export class PluginsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getExpiringSoon(limit = 5) {
|
async getExpiringSoon(limit = 5) {
|
||||||
|
try {
|
||||||
const plugins = await this.prisma.pluginSubscription.findMany({
|
const plugins = await this.prisma.pluginSubscription.findMany({
|
||||||
where: {
|
where: {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
expiresAt: { not: null },
|
expiresAt: { not: null },
|
||||||
tenant: { isActive: true },
|
tenant: { isActive: true },
|
||||||
},
|
},
|
||||||
include: {
|
select: {
|
||||||
|
id: true,
|
||||||
|
tenantId: true,
|
||||||
|
name: true,
|
||||||
|
specs: true,
|
||||||
|
priceRials: true,
|
||||||
|
chargedAt: true,
|
||||||
|
planMonths: true,
|
||||||
|
expiresAt: true,
|
||||||
|
description: true,
|
||||||
|
alertDaysBefore: true,
|
||||||
|
lastAlertDate: true,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
tenant: {
|
tenant: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -117,7 +126,7 @@ export class PluginsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
.map((p) => ({
|
.map((p) => ({
|
||||||
...this.toSummary(p),
|
...this.toSummary(p as PluginSubscription),
|
||||||
id: p.id,
|
id: p.id,
|
||||||
tenantId: p.tenantId,
|
tenantId: p.tenantId,
|
||||||
tenant: p.tenant,
|
tenant: p.tenant,
|
||||||
@@ -132,25 +141,31 @@ export class PluginsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return ae - be;
|
return ae - be;
|
||||||
})
|
})
|
||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`getExpiringSoon failed: ${error?.message || error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setStoreEnabled(tenantId: string, enabled: boolean, userId?: string) {
|
||||||
|
return this.store.setStoreEnabled(tenantId, enabled, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTenantPlugins(tenantId: string) {
|
async getTenantPlugins(tenantId: string) {
|
||||||
const tenant = await this.requireTenant(tenantId);
|
const tenant = await this.requireTenant(tenantId);
|
||||||
const plugins = await this.prisma.pluginSubscription.findMany({
|
const plugins = await this.loadTenantPlugins(tenantId);
|
||||||
where: { tenantId },
|
|
||||||
orderBy: [{ isActive: 'desc' }, { name: 'asc' }],
|
|
||||||
include: {
|
|
||||||
charges: { orderBy: { createdAt: 'desc' }, take: 10 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const catalogKeys =
|
let catalogKeys: { settingKey: string; titleFa: string }[] = [];
|
||||||
tenant.product === 'khanegi'
|
if (tenant.product === 'khanegi') {
|
||||||
? await this.prisma.pluginCatalogItem.findMany({
|
try {
|
||||||
|
catalogKeys = await this.prisma.pluginCatalogItem.findMany({
|
||||||
select: { settingKey: true, titleFa: true },
|
select: { settingKey: true, titleFa: true },
|
||||||
orderBy: { titleFa: 'asc' },
|
orderBy: { titleFa: 'asc' },
|
||||||
})
|
});
|
||||||
: [];
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`catalogKeys skipped: ${error?.message || error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tenant: {
|
tenant: {
|
||||||
@@ -161,10 +176,12 @@ export class PluginsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
nameEn: tenant.nameEn,
|
nameEn: tenant.nameEn,
|
||||||
domain: tenant.domain,
|
domain: tenant.domain,
|
||||||
avatarUrl: tenant.avatarUrl,
|
avatarUrl: tenant.avatarUrl,
|
||||||
|
pluginStoreEnabled:
|
||||||
|
tenant.product === 'khanegi' && (await this.store.isStoreEnabled(tenantId)),
|
||||||
},
|
},
|
||||||
plugins: plugins.map((p) => ({
|
plugins: plugins.map((p) => ({
|
||||||
...this.publicPlugin(p),
|
...this.publicPlugin(p as PluginSubscription),
|
||||||
summary: this.toSummary(p),
|
summary: this.toSummary(p as PluginSubscription),
|
||||||
charges: p.charges,
|
charges: p.charges,
|
||||||
})),
|
})),
|
||||||
catalogKeys,
|
catalogKeys,
|
||||||
@@ -590,4 +607,82 @@ export class PluginsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
if (!tenant) throw new NotFoundException('نسخه یافت نشد');
|
if (!tenant) throw new NotFoundException('نسخه یافت نشد');
|
||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async loadTenantPlugins(tenantId: string) {
|
||||||
|
const orderBy = [{ isActive: 'desc' as const }, { name: 'asc' as const }];
|
||||||
|
const charges = { orderBy: { createdAt: 'desc' as const }, take: 10 };
|
||||||
|
try {
|
||||||
|
return await this.prisma.pluginSubscription.findMany({
|
||||||
|
where: { tenantId },
|
||||||
|
orderBy,
|
||||||
|
include: { charges },
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(`loadTenantPlugins fallback: ${error?.message || error}`);
|
||||||
|
try {
|
||||||
|
return await this.prisma.pluginSubscription.findMany({
|
||||||
|
where: { tenantId },
|
||||||
|
orderBy,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
tenantId: true,
|
||||||
|
name: true,
|
||||||
|
specs: true,
|
||||||
|
priceRials: true,
|
||||||
|
chargedAt: true,
|
||||||
|
planMonths: true,
|
||||||
|
expiresAt: true,
|
||||||
|
description: true,
|
||||||
|
alertDaysBefore: true,
|
||||||
|
lastAlertDate: true,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
charges,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (inner: any) {
|
||||||
|
this.logger.error(`loadTenantPlugins failed: ${inner?.message || inner}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadTenantRows() {
|
||||||
|
try {
|
||||||
|
return await this.prisma.tenant.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
||||||
|
include: {
|
||||||
|
pluginSubscriptions: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
tenantId: true,
|
||||||
|
name: true,
|
||||||
|
specs: true,
|
||||||
|
priceRials: true,
|
||||||
|
chargedAt: true,
|
||||||
|
planMonths: true,
|
||||||
|
expiresAt: true,
|
||||||
|
description: true,
|
||||||
|
alertDaysBefore: true,
|
||||||
|
lastAlertDate: true,
|
||||||
|
isActive: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`listTenants plugins skipped: ${error?.message || error}`);
|
||||||
|
const rows = await this.prisma.tenant.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
||||||
|
});
|
||||||
|
return rows.map((tenant) => ({ ...tenant, pluginSubscriptions: [] }));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/** کلیدهای runtime / لاگ — مال سوپرادمین نیستند؛ در اپ خانگی توسط cron پر میشوند. */
|
/** کلیدهای runtime / لاگ — مال سوپرادمین نیستند؛ در اپ خانگی توسط cron پر میشوند. */
|
||||||
|
export const PLUGIN_STORE_SETTING_KEY = 'plugin_store_enabled';
|
||||||
|
|
||||||
export const RUNTIME_PROGRAM_SETTING_KEYS = new Set([
|
export const RUNTIME_PROGRAM_SETTING_KEYS = new Set([
|
||||||
'montzar_last_product_log',
|
'montzar_last_product_log',
|
||||||
'montzar_order_response',
|
'montzar_order_response',
|
||||||
@@ -13,6 +15,7 @@ export const RUNTIME_PROGRAM_SETTING_KEYS = new Set([
|
|||||||
'down_price',
|
'down_price',
|
||||||
'price_extreme_date',
|
'price_extreme_date',
|
||||||
'do_send_liman',
|
'do_send_liman',
|
||||||
|
PLUGIN_STORE_SETTING_KEY,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function isRuntimeProgramSettingKey(keyRaw: string): boolean {
|
export function isRuntimeProgramSettingKey(keyRaw: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user