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:
@@ -21,8 +21,8 @@ import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AdminNotifyService } from '../notifications/admin-notify.service';
|
||||
import { CentralSmsService } from '../notifications/central-sms.service';
|
||||
import { RuntimeSettingsService } from '../notifications/runtime-settings.service';
|
||||
import { ProgramSettingsService } from '../program-settings/program-settings.service';
|
||||
import { PLUGIN_STORE_SETTING_KEY } from '../program-settings/runtime-setting-keys';
|
||||
import {
|
||||
addIranMonths,
|
||||
addIranPlanExpiry,
|
||||
@@ -55,7 +55,6 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly audit: AuditService,
|
||||
private readonly notify: AdminNotifyService,
|
||||
private readonly sms: CentralSmsService,
|
||||
private readonly runtime: RuntimeSettingsService,
|
||||
private readonly programSettings: ProgramSettingsService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
@@ -135,6 +134,69 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
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) {
|
||||
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId } });
|
||||
if (!tenant || tenant.product !== ProductType.khanegi) return;
|
||||
@@ -148,12 +210,17 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
async listCatalogAdmin() {
|
||||
await this.seedCatalog();
|
||||
const items = await this.prisma.pluginCatalogItem.findMany({
|
||||
orderBy: { titleFa: 'asc' },
|
||||
include: { prices: { orderBy: { durationDays: 'asc' } } },
|
||||
});
|
||||
return items.map((item) => this.toCatalogAdmin(item));
|
||||
try {
|
||||
await this.seedCatalog();
|
||||
const items = await this.prisma.pluginCatalogItem.findMany({
|
||||
orderBy: { titleFa: 'asc' },
|
||||
include: { prices: { orderBy: { durationDays: 'asc' } } },
|
||||
});
|
||||
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) {
|
||||
@@ -254,23 +321,28 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
async listRequestsAdmin(status?: string) {
|
||||
const where: Prisma.PluginPurchaseRequestWhereInput = {
|
||||
tenant: { product: ProductType.khanegi },
|
||||
};
|
||||
if (status && ['pending', 'approved', 'rejected', 'cancelled'].includes(status)) {
|
||||
where.status = status as PluginPurchaseStatus;
|
||||
}
|
||||
const rows = await this.prisma.pluginPurchaseRequest.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
tenant: {
|
||||
select: { id: true, nameFa: true, domain: true, slug: true, product: true },
|
||||
try {
|
||||
const where: Prisma.PluginPurchaseRequestWhereInput = {
|
||||
tenant: { product: ProductType.khanegi },
|
||||
};
|
||||
if (status && ['pending', 'approved', 'rejected', 'cancelled'].includes(status)) {
|
||||
where.status = status as PluginPurchaseStatus;
|
||||
}
|
||||
const rows = await this.prisma.pluginPurchaseRequest.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
tenant: {
|
||||
select: { id: true, nameFa: true, domain: true, slug: true, product: true },
|
||||
},
|
||||
catalogItem: { select: { id: true, titleFa: true, settingKey: true } },
|
||||
},
|
||||
catalogItem: { select: { id: true, titleFa: true, settingKey: true } },
|
||||
},
|
||||
});
|
||||
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) {
|
||||
@@ -396,12 +468,8 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
remoteStoreEnabled() {
|
||||
return this.runtime.getPluginStoreEnabled();
|
||||
}
|
||||
|
||||
async remoteStore(tenantId: string, product: string) {
|
||||
this.assertRemoteKhanegi(tenantId, product);
|
||||
await this.assertRemoteKhanegi(tenantId, product);
|
||||
const items = await this.prisma.pluginCatalogItem.findMany({
|
||||
where: { inStore: true },
|
||||
orderBy: { titleFa: 'asc' },
|
||||
@@ -462,7 +530,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
async remoteListRequests(tenantId: string, product: string) {
|
||||
this.assertRemoteKhanegi(tenantId, product);
|
||||
await this.assertRemoteKhanegi(tenantId, product);
|
||||
const rows = await this.prisma.pluginPurchaseRequest.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -479,7 +547,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
product: string,
|
||||
dto: CreatePurchaseRequestDto,
|
||||
) {
|
||||
this.assertRemoteKhanegi(tenantId, product);
|
||||
await this.assertRemoteKhanegi(tenantId, product);
|
||||
const item = await this.prisma.pluginCatalogItem.findUnique({
|
||||
where: { id: dto.catalogItemId },
|
||||
include: { prices: true },
|
||||
@@ -541,7 +609,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
async remoteCancelRequest(tenantId: string, product: string, requestId: string) {
|
||||
this.assertRemoteKhanegi(tenantId, product);
|
||||
await this.assertRemoteKhanegi(tenantId, product);
|
||||
const req = await this.prisma.pluginPurchaseRequest.findFirst({
|
||||
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') {
|
||||
throw new ForbiddenException('فروشگاه افزونه فقط برای خانگی است');
|
||||
}
|
||||
if (!this.runtime.getPluginStoreEnabled()) {
|
||||
throw new ForbiddenException('فروشگاه افزونه خاموش است');
|
||||
}
|
||||
if (!tenantId) throw new ForbiddenException('نسخه نامعتبر است');
|
||||
if (!(await this.isStoreEnabled(tenantId))) {
|
||||
throw new ForbiddenException('فروشگاه افزونه برای این نسخه خاموش است');
|
||||
}
|
||||
}
|
||||
|
||||
private assertPrices(prices: SaveCatalogItemDto['prices']) {
|
||||
|
||||
Reference in New Issue
Block a user