fix(settings): restrict superadmin template to can_edit=0 keys only

Superadmin was seeding all deploy_settings_const INSERT keys including
can_edit=1 entries with wrong defaults (e.g. montzar_product_id=25).
Template/catalog now follow final can_edit=0 from SQL; editable keys are
purged from setting_templates and tenant settings on boot/seed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-19 18:40:07 +03:30
co-authored by Cursor
parent 0b2e4b05f9
commit 71d6f6a534
6 changed files with 958 additions and 1589 deletions
@@ -1,20 +1,13 @@
/**
* کلیدهای INSERT در khaneghi_server/database/sql/deploy_settings_const.sql
* منبع قالب ایمپورت سوپرادمین — داخل باندل تا به فایل prisma وابسته نباشد.
* این ۱۹۵ کلید با SQL فعلی هم‌تراز است؛ کلید داخل این مجموعه در مودال «تصمیم برای هر کلید» نمی‌آید.
* کلیدهای can_edit=0 در deploy_settings_const.sql — فقط همین‌ها در قالب سوپرادمین.
* تولید خودکار: node scripts/sync-superadmin-setting-template.mjs
*/
export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"accounting_provider",
"admin_url",
"apk_download_link",
"app_url",
"auto_active_customer",
"balance_report_exclude_customer_id",
"bale_bot_token",
"banner_enabled",
"buy_fee",
"change_mesghal",
"charge_notif",
"chart_id",
"chart_use_local_data",
"chat_api",
@@ -23,24 +16,12 @@ export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"chat_upload_url",
"chat_webhook_base_url",
"chat_webhook_secret",
"close_price",
"customer_direct_sms",
"customer_special_pricing",
"deposit_account_holder",
"deposit_account_number",
"deposit_card_number",
"deposit_method_card_to_card",
"deposit_method_gateway",
"deposit_method_identified",
"deposit_method_shetab",
"deposit_sheba",
"deposit_sms_support_number",
"deposit_support_message_text",
"down_price",
"enable_conditional_order",
"enable_customer_level_profit",
"enable_product_funds",
"endDay",
"external_order_api_enabled",
"external_payment_base_url",
"external_payment_client_id",
@@ -49,33 +30,19 @@ export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"external_payment_gateway",
"external_payment_return_url",
"external_payment_webhook_url",
"fee_conditional_order",
"fee_deduct_from_gold",
"fee_free",
"fee_service_charge_percentage",
"gift_for_referral",
"gift_for_referrer",
"group_sms",
"has_chat",
"has_page_earn_money",
"has_page_gift_card",
"has_page_notification",
"has_page_price_trigger",
"invite_link_text",
"is_store",
"jibit_enabled",
"jibit_verification",
"logo_login",
"logo_register",
"main_metal",
"main_product",
"main_product_price",
"main_wallet_products",
"metal_type",
"min_deposit",
"min_first_buy",
"min_withdraw",
"minimum_sell",
"montify_name",
"montzar_base_url",
"montzar_closed",
@@ -83,19 +50,12 @@ export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"montzar_last_product_log",
"montzar_live_price",
"montzar_mazaneh",
"montzar_mobile",
"montzar_open_buy",
"montzar_open_sell",
"montzar_order_response",
"montzar_password",
"montzar_product_id",
"montzar_token",
"montzar_truly_buy_mazaneh",
"montzar_truly_sell_mazaneh",
"name_fa",
"node_client_id",
"node_shared_secret",
"open_register",
"pargar_api_token",
"pargar_item_id_gold",
"pargar_item_id_rial",
@@ -114,23 +74,83 @@ export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"permision__enabled",
"price_channel_access",
"price_channel_bale_to",
"price_channel_rubika_to",
"price_channel_telegram_to",
"price_extreme_date",
"referral_bonus_amount",
"rubika_bot_token",
"send_to_pargar",
"send_to_tahesab",
"show_to_users_chat",
"sms_panel",
"sse_path",
"tahesab_bank_account",
"tahesab_db",
"tahesab_host",
"tahesab_port",
"tahesab_token",
"telegram_bot_token",
"unit",
"up_price",
"wallet_payment_provider",
"wss_enabled",
"zarin_api",
"zarin_gorohe_kar",
"zarin_name_g",
"zarin_noe_kar",
"zarin_noe_kar_tedadi",
"zarin_token",
"zarin_uid_prefix",
]);
/** کلیدهای can_edit=1 — نباید در قالب/جدول settings سوپرادمین باشند */
export const DEPLOY_SETTING_EDITABLE_KEYS = new Set<string>([
"auto_active_customer",
"balance_report_exclude_customer_id",
"banner_enabled",
"buy_fee",
"change_mesghal",
"charge_notif",
"close_price",
"deposit_account_holder",
"deposit_account_number",
"deposit_card_number",
"deposit_method_card_to_card",
"deposit_method_gateway",
"deposit_method_identified",
"deposit_method_shetab",
"deposit_sheba",
"deposit_sms_support_number",
"endDay",
"fee_conditional_order",
"fee_free",
"fee_service_charge_percentage",
"gift_for_referral",
"gift_for_referrer",
"has_chat",
"jibit_verification",
"main_product_price",
"min_deposit",
"min_first_buy",
"min_withdraw",
"minimum_sell",
"montzar_mobile",
"montzar_open_buy",
"montzar_open_sell",
"montzar_password",
"montzar_product_id",
"name_fa",
"open_register",
"price_channel_diff",
"price_channel_enabled",
"price_channel_footer_id",
"price_channel_footer_link",
"price_channel_footer_phone",
"price_channel_message_text",
"price_channel_messengers",
"price_channel_min_seconds",
"price_channel_rubika_to",
"price_channel_telegram_to",
"price_extreme_date",
"price_provider",
"products_site_link_enabled",
"products_site_link_text",
"products_site_link_url",
"profit_gold_api_enabled",
"referral_bonus_amount",
"referral_gift_buy_from_montzar",
"referral_gift_on_first_order",
"referral_id",
@@ -140,18 +160,13 @@ export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"register_referral_bonus_message",
"register_referral_sms_to_referrer",
"require_zero_main_gold_before_other_buy",
"rubika_bot_token",
"sell_fee",
"send_to_pargar",
"send_to_tahesab",
"service_charge_percentage",
"show_gold_ounce_price",
"show_home_price_when_closed",
"show_last_price",
"show_product_card_meta",
"show_product_details",
"show_product_trade_status",
"show_to_users_chat",
"sms_notif_bank_card_admin",
"sms_notif_bank_card_customer",
"sms_notif_fund_withdraw_admin",
@@ -168,36 +183,11 @@ export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"sms_notif_wallet_alert",
"sms_notif_withdraw_admin",
"sms_notif_withdraw_otp",
"sms_panel",
"sse_path",
"startDay",
"support_messenger",
"support_messenger_id",
"support_messenger_text",
"tahesab_bank_account",
"tahesab_db",
"tahesab_host",
"tahesab_port",
"tahesab_token",
"telegram_bot_token",
"text_about",
"text_rule",
"title_fav",
"title_pwa",
"tracking_systems",
"tracking_systems_enabled",
"unit",
"up_price",
"wallet_payment_provider",
"wss_enabled",
"zaarbox_api_url",
"zaarbox_price_key",
"zarin_api",
"zarin_gorohe_kar",
"zarin_name_g",
"zarin_noe_kar",
"zarin_noe_kar_tedadi",
"zarin_token",
"zarin_uid_prefix",
]);
@@ -17,7 +17,10 @@ import { AuditService } from '../audit/audit.service';
import { ReplicaWatchService } from '../replica-watch/replica-watch.service';
import { isSecretOrServerOnlySettingKey } from './secret-setting-keys';
import { isRuntimeProgramSettingKey } from './runtime-setting-keys';
import { DEPLOY_SETTING_CATALOG_KEYS } from './deploy-setting-catalog-keys';
import {
DEPLOY_SETTING_CATALOG_KEYS,
DEPLOY_SETTING_EDITABLE_KEYS,
} from './deploy-setting-catalog-keys';
export type ProgramSettingRow = {
id?: string | number;
@@ -67,6 +70,12 @@ export class ProgramSettingsService implements OnModuleInit {
if (result.seeded) {
this.logger.log(`setting_templates seeded (${result.count} keys)`);
}
const purged = await this.purgeEditableFromSuperAdmin();
if (purged.templates || purged.settings) {
this.logger.log(
`purged can_edit=1 from superadmin (templates=${purged.templates}, settings=${purged.settings})`,
);
}
await this.seedEmptyKhanegiTenants();
} catch (error: any) {
this.logger.warn(`setting_templates seed skipped: ${error?.message || error}`);
@@ -113,7 +122,8 @@ export class ProgramSettingsService implements OnModuleInit {
const tenant = await this.requireKhanegiTenant(tenantId);
const incoming = this.asSettingsArray(itemsRaw)
.map((row) => this.normalizeIncoming(row))
.filter((row) => row.key && !isRuntimeProgramSettingKey(row.key));
.filter((row) => row.key && !isRuntimeProgramSettingKey(row.key))
.filter((row) => this.isSystemProgramSettingKey(row.key));
if (!incoming.length) {
throw new BadRequestException('هیچ تنظیم معتبری برای ذخیره ارسال نشده');
@@ -292,6 +302,7 @@ export class ProgramSettingsService implements OnModuleInit {
await this.ensureTemplateDefaultsForTenant(tx, tenantId);
for (const [key, row] of remoteByKey) {
if (!this.isSystemProgramSettingKey(key)) continue;
const inCatalog = catalogKeys.has(key);
const scope = inCatalog ? 'this' : (decisionMap.get(key) ?? 'this');
if (scope === 'none') continue;
@@ -346,7 +357,10 @@ export class ProgramSettingsService implements OnModuleInit {
async listTemplates() {
await this.ensureTemplatesSeeded();
const rows = await this.prisma.settingTemplate.findMany({
where: { deletedAt: null },
where: {
deletedAt: null,
key: { in: [...DEPLOY_SETTING_CATALOG_KEYS] },
},
orderBy: { key: 'asc' },
});
return {
@@ -359,7 +373,8 @@ export class ProgramSettingsService implements OnModuleInit {
async addKeys(itemsRaw: Array<Record<string, unknown>>, userId?: string) {
const incoming = this.asSettingsArray(itemsRaw)
.map((row) => this.normalizeIncoming(row))
.filter((row) => row.key && !isRuntimeProgramSettingKey(row.key));
.filter((row) => row.key && !isRuntimeProgramSettingKey(row.key))
.filter((row) => this.isSystemProgramSettingKey(row.key));
if (!incoming.length) {
throw new BadRequestException('هیچ کلید معتبری ارسال نشده');
}
@@ -440,7 +455,10 @@ export class ProgramSettingsService implements OnModuleInit {
async seedTenantFromTemplates(tenantId: string) {
await this.ensureTemplatesSeeded();
const templates = await this.prisma.settingTemplate.findMany({
where: { deletedAt: null },
where: {
deletedAt: null,
key: { in: [...DEPLOY_SETTING_CATALOG_KEYS] },
},
});
if (!templates.length) return { count: 0 };
@@ -479,6 +497,8 @@ export class ProgramSettingsService implements OnModuleInit {
for (const row of raw) {
const key = String(row.key || '').trim();
if (!key || isRuntimeProgramSettingKey(key)) continue;
if (!this.isSystemProgramSettingKey(key)) continue;
if (this.toSmallInt(row.can_edit, 0) !== 0) continue;
const existing = await tx.settingTemplate.findUnique({ where: { key } });
if (existing) {
if (existing.deletedAt) {
@@ -521,17 +541,37 @@ export class ProgramSettingsService implements OnModuleInit {
}
}
/**
* قالب ایمپورت = کلیدهای INSERT در deploy_settings_const.sql
* (باندل + JSON هم‌تراز همان فایل). جدول setting_templates منبع قالب نیست.
*/
/** فقط can_edit=0 نهایی deploy_settings_const — نه کل INSERTها */
private deployCatalogKeySet() {
const keys = new Set<string>(DEPLOY_SETTING_CATALOG_KEYS);
for (const row of this.readSeedRows()) {
const key = String(row.key || '').trim();
if (key) keys.add(key);
return new Set<string>(DEPLOY_SETTING_CATALOG_KEYS);
}
private isSystemProgramSettingKey(key: string) {
const k = String(key || '').trim();
if (!k || isRuntimeProgramSettingKey(k)) return false;
if (DEPLOY_SETTING_EDITABLE_KEYS.has(k)) return false;
return DEPLOY_SETTING_CATALOG_KEYS.has(k);
}
/** حذف can_edit=1 از قالب و همهٔ پروژه‌های خانگی در DB سوپرادمین */
async purgeEditableFromSuperAdmin() {
const keys = [...DEPLOY_SETTING_EDITABLE_KEYS];
if (!keys.length) return { templates: 0, settings: 0 };
const result = await this.prisma.$transaction(async (tx: any) => {
const templates = await tx.settingTemplate.deleteMany({
where: { key: { in: keys } },
});
const settings = await tx.setting.deleteMany({
where: { key: { in: keys } },
});
return { templates: templates.count, settings: settings.count };
});
if (result.templates || result.settings) {
this.replicaWatch.kick();
}
return keys;
return result;
}
private deployCatalogCanEditMap() {
@@ -571,6 +611,7 @@ export class ProgramSettingsService implements OnModuleInit {
});
return rows
.filter((row: Setting) => !isRuntimeProgramSettingKey(row.key))
.filter((row: Setting) => this.isSystemProgramSettingKey(row.key))
.map((row: Setting) => this.toApiRow(row));
}
@@ -656,7 +697,12 @@ export class ProgramSettingsService implements OnModuleInit {
tx: any,
tenantId: string,
) {
const templates = await tx.settingTemplate.findMany({ where: { deletedAt: null } });
const templates = await tx.settingTemplate.findMany({
where: {
deletedAt: null,
key: { in: [...DEPLOY_SETTING_CATALOG_KEYS] },
},
});
for (const tpl of templates) {
const existing = await tx.setting.findUnique({
where: { tenantId_key: { tenantId, key: tpl.key } },
@@ -36,7 +36,15 @@ export class SettingTemplatesController {
@Post('seed')
@RequirePermissions('khanegi:write')
seed() {
return this.programSettings.ensureTemplatesSeeded();
async seed() {
const seeded = await this.programSettings.ensureTemplatesSeeded();
const purged = await this.programSettings.purgeEditableFromSuperAdmin();
return { ...seeded, purged };
}
@Post('purge-editable')
@RequirePermissions('khanegi:write')
purgeEditable() {
return this.programSettings.purgeEditableFromSuperAdmin();
}
}