2026-08-19 18:40:07 +03:30
|
|
|
#!/usr/bin/env node
|
|
|
|
|
/**
|
|
|
|
|
* منبع حقیقت: khaneghi_server/database/sql/deploy_settings_const.sql
|
|
|
|
|
* خروجی:
|
2026-08-23 22:34:25 +03:30
|
|
|
* - prisma/setting-templates-from-deploy.json (فقط کلیدهای مالکیت سوپرادمین)
|
2026-08-19 18:40:07 +03:30
|
|
|
* - src/program-settings/deploy-setting-catalog-keys.ts
|
2026-08-19 18:49:08 +03:30
|
|
|
* - ../controlpanel_user/src/config/superAdminSettingKeys.ts
|
2026-08-19 21:47:43 +03:30
|
|
|
*
|
2026-08-23 22:34:25 +03:30
|
|
|
* مالکیت سوپرادمین دیگر با can_edit تعیین نمیشود: فقط کلیدهای SUPERADMIN_OWNED_KEYS
|
|
|
|
|
* (تب «دسترسی مشتری به قابلیتها») در سوپرادمین میمانند و بقیه مالک دیتابیس هر نصباند.
|
|
|
|
|
* کلیدهای لوکال در جدول settings همان can_edit=0 خودشان را نگه میدارند تا در تب
|
|
|
|
|
* «تنظیمات پیشرفته» پنل خانگی بمانند؛ اینجا فقط از قالب سوپرادمین بیرون میروند.
|
2026-08-19 18:40:07 +03:30
|
|
|
*/
|
|
|
|
|
import { readFileSync, writeFileSync } from 'fs';
|
|
|
|
|
import { dirname, join } from 'path';
|
|
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
|
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
const root = join(__dirname, '..');
|
|
|
|
|
const sqlPath = join(
|
|
|
|
|
root,
|
|
|
|
|
'../khaneghi_server/database/sql/deploy_settings_const.sql',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const sql = readFileSync(sqlPath, 'utf8');
|
|
|
|
|
|
|
|
|
|
/** @type {Map<string, { key: string, value: string, title_fa: string, can_edit: number, is_active: number, description: string, type: string }>} */
|
|
|
|
|
const rows = new Map();
|
|
|
|
|
|
|
|
|
|
function unquote(s) {
|
|
|
|
|
return String(s ?? '')
|
|
|
|
|
.trim()
|
|
|
|
|
.replace(/^'/, '')
|
|
|
|
|
.replace(/'$/, '')
|
|
|
|
|
.replace(/\\'/g, "'")
|
|
|
|
|
.replace(/\\n/g, '\n');
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 21:47:43 +03:30
|
|
|
function parseSqlList(expr) {
|
|
|
|
|
const out = [];
|
|
|
|
|
let i = 0;
|
|
|
|
|
const s = String(expr ?? '').trim();
|
|
|
|
|
while (i < s.length) {
|
|
|
|
|
while (i < s.length && /[\s,]/.test(s[i])) i += 1;
|
|
|
|
|
if (i >= s.length) break;
|
|
|
|
|
if (s[i] === "'") {
|
|
|
|
|
i += 1;
|
|
|
|
|
let buf = '';
|
|
|
|
|
while (i < s.length) {
|
|
|
|
|
if (s[i] === '\\' && i + 1 < s.length) {
|
|
|
|
|
buf += s[i + 1];
|
|
|
|
|
i += 2;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (s[i] === "'") {
|
|
|
|
|
if (s[i + 1] === "'") {
|
|
|
|
|
buf += "'";
|
|
|
|
|
i += 2;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
i += 1;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
buf += s[i];
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
out.push(buf);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let buf = '';
|
|
|
|
|
while (i < s.length && s[i] !== ',') {
|
|
|
|
|
buf += s[i];
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
out.push(buf.trim());
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureRow(key) {
|
|
|
|
|
const k = String(key || '').trim();
|
|
|
|
|
if (!k) return null;
|
|
|
|
|
let row = rows.get(k);
|
|
|
|
|
if (!row) {
|
|
|
|
|
row = {
|
|
|
|
|
key: k,
|
|
|
|
|
value: '',
|
|
|
|
|
title_fa: k,
|
|
|
|
|
can_edit: 0,
|
|
|
|
|
is_active: 1,
|
|
|
|
|
description: '',
|
|
|
|
|
type: '',
|
|
|
|
|
};
|
|
|
|
|
rows.set(k, row);
|
|
|
|
|
}
|
|
|
|
|
return row;
|
2026-08-19 18:40:07 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyCanEdit(block, value) {
|
|
|
|
|
const keys = [...block.matchAll(/'([^']+)'/g)].map((x) => x[1]);
|
|
|
|
|
for (const key of keys) {
|
2026-08-19 21:47:43 +03:30
|
|
|
const row = ensureRow(key);
|
2026-08-19 18:40:07 +03:30
|
|
|
if (row) row.can_edit = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 21:47:43 +03:30
|
|
|
function isBlankSql(v) {
|
|
|
|
|
const t = String(v ?? '').trim();
|
|
|
|
|
return t === '' || t === 'NULL' || t.startsWith('@');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// INSERT INTO settings (cols) SELECT ...
|
|
|
|
|
for (const m of sql.matchAll(
|
|
|
|
|
/INSERT INTO `settings` \(([^)]+)\)\s*SELECT\s+([\s\S]*?)\nWHERE NOT EXISTS/g,
|
|
|
|
|
)) {
|
|
|
|
|
const cols = m[1].split(',').map((c) => c.replace(/[`\s]/g, ''));
|
|
|
|
|
const values = parseSqlList(m[2]);
|
|
|
|
|
const rec = {};
|
|
|
|
|
cols.forEach((col, i) => {
|
|
|
|
|
rec[col] = values[i];
|
|
|
|
|
});
|
|
|
|
|
const key = rec.key;
|
|
|
|
|
if (!key || isBlankSql(key)) continue;
|
|
|
|
|
const row = ensureRow(key);
|
|
|
|
|
if (!isBlankSql(rec.value) && rec.value !== undefined) row.value = unquote(rec.value);
|
|
|
|
|
if (!isBlankSql(rec.title_fa) && rec.title_fa !== undefined) row.title_fa = unquote(rec.title_fa);
|
|
|
|
|
if (rec.can_edit !== undefined && !isBlankSql(rec.can_edit)) row.can_edit = Number(rec.can_edit);
|
|
|
|
|
if (rec.is_active !== undefined && !isBlankSql(rec.is_active)) row.is_active = Number(rec.is_active);
|
|
|
|
|
if (!isBlankSql(rec.description) && rec.description !== undefined) {
|
|
|
|
|
row.description = unquote(rec.description);
|
|
|
|
|
}
|
|
|
|
|
if (!isBlankSql(rec.type) && rec.type !== undefined) row.type = unquote(rec.type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UPDATE can_edit = 0/1 batches (بعد از INSERT — منبع حقیقت نهایی)
|
2026-08-19 18:40:07 +03:30
|
|
|
for (const m of sql.matchAll(
|
|
|
|
|
/UPDATE\s+`settings`\s+SET\s+`can_edit`\s*=\s*(\d)(?:,\s*`[^`]+`\s*=[^W]+)?\s+WHERE\s+`key`\s+IN\s*\(([\s\S]*?)\)\s*;/g,
|
|
|
|
|
)) {
|
|
|
|
|
applyCanEdit(m[2], Number(m[1]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const m of sql.matchAll(
|
|
|
|
|
/UPDATE\s+`settings`\s+SET\s+`can_edit`\s*=\s*(\d)\s+WHERE\s+`key`\s*=\s*'([^']+)'/g,
|
|
|
|
|
)) {
|
2026-08-19 21:47:43 +03:30
|
|
|
const row = ensureRow(m[2]);
|
2026-08-19 18:40:07 +03:30
|
|
|
if (row) row.can_edit = Number(m[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (/UPDATE\s+`settings`\s+SET\s+`can_edit`\s*=\s*0,\s*`is_active`\s*=\s*1\s+WHERE\s+`key`\s+LIKE\s+'zarin_%'/.test(sql)) {
|
|
|
|
|
for (const [key, row] of rows) {
|
|
|
|
|
if (key.startsWith('zarin_')) row.can_edit = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const m of sql.matchAll(
|
|
|
|
|
/UPDATE\s+`settings`\s+SET\s+`type`\s*=\s*'([^']+)'\s+WHERE\s+`key`\s+IN\s*\(([\s\S]*?)\)\s*;/g,
|
|
|
|
|
)) {
|
|
|
|
|
const type = m[1];
|
|
|
|
|
for (const key of [...m[2].matchAll(/'([^']+)'/g)].map((x) => x[1])) {
|
|
|
|
|
const row = rows.get(key);
|
|
|
|
|
if (row && type) row.type = type;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 21:47:43 +03:30
|
|
|
for (const m of sql.matchAll(
|
|
|
|
|
/UPDATE\s+`settings`\s+SET\s+`title_fa`\s*=\s+'((?:\\'|[^'])*)'\s+WHERE\s+`key`\s*=\s*'([^']+)'/g,
|
|
|
|
|
)) {
|
|
|
|
|
const row = rows.get(m[2]);
|
|
|
|
|
if (row && m[1]) row.title_fa = unquote(`'${m[1]}'`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const m of sql.matchAll(
|
|
|
|
|
/UPDATE\s+`settings`\s+SET\s+`description`\s*=\s+'((?:\\'|[^'])*)'\s+WHERE\s+`key`\s*=\s*'([^']+)'/g,
|
|
|
|
|
)) {
|
|
|
|
|
const row = rows.get(m[2]);
|
|
|
|
|
if (row && m[1]) row.description = unquote(`'${m[1]}'`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SECRET_OR_PROJECT_VALUE = /(token|password|secret|api_key|sheba|card_number|client_secret|client_id|username)$/i;
|
|
|
|
|
const EMPTY_TEMPLATE_VALUE_KEYS = new Set([
|
|
|
|
|
'main_product',
|
|
|
|
|
'main_wallet_products',
|
|
|
|
|
'payping_fin_customer_id',
|
|
|
|
|
'payping_fin_url',
|
|
|
|
|
'payping_start_url',
|
|
|
|
|
'payping_verify_url',
|
|
|
|
|
'price_provider',
|
|
|
|
|
'pargar_item_id_gold',
|
|
|
|
|
'pargar_item_id_rial',
|
|
|
|
|
'zarin_noe_kar',
|
|
|
|
|
'zarin_noe_kar_tedadi',
|
|
|
|
|
'zarin_gorohe_kar',
|
|
|
|
|
'zarin_name_g',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
function templateValue(row) {
|
|
|
|
|
const key = row.key;
|
|
|
|
|
if (SECRET_OR_PROJECT_VALUE.test(key) || EMPTY_TEMPLATE_VALUE_KEYS.has(key)) return '';
|
|
|
|
|
const v = String(row.value ?? '');
|
|
|
|
|
if (/^eyJ/.test(v) || v.startsWith('http') && /token|secret/i.test(key)) return '';
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EXTRA_EDITABLE = ['fee', 'referral_bonus_count'];
|
|
|
|
|
for (const key of EXTRA_EDITABLE) {
|
|
|
|
|
const row = ensureRow(key);
|
|
|
|
|
if (row) row.can_edit = 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 22:34:25 +03:30
|
|
|
/** تنها کلیدهایی که سوپرادمین مالکشان میماند — گروه «دسترسی مشتری به قابلیتها» */
|
|
|
|
|
const SUPERADMIN_OWNED_KEYS = new Set([
|
|
|
|
|
'customer_direct_sms',
|
|
|
|
|
'customer_special_pricing',
|
|
|
|
|
'enable_conditional_order',
|
|
|
|
|
'enable_customer_level_profit',
|
|
|
|
|
'enable_product_funds',
|
|
|
|
|
'group_sms',
|
|
|
|
|
'has_page_earn_money',
|
|
|
|
|
'has_page_gift_card',
|
|
|
|
|
'has_page_notification',
|
|
|
|
|
'has_page_price_trigger',
|
|
|
|
|
'jibit_enabled',
|
|
|
|
|
'permision__enabled',
|
|
|
|
|
'show_to_users_chat',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-08-19 18:40:07 +03:30
|
|
|
const systemRows = [...rows.values()]
|
2026-08-23 22:34:25 +03:30
|
|
|
.filter((r) => r.can_edit === 0 && SUPERADMIN_OWNED_KEYS.has(r.key))
|
2026-08-19 18:40:07 +03:30
|
|
|
.sort((a, b) => a.key.localeCompare(b.key));
|
|
|
|
|
|
|
|
|
|
const editableRows = [...rows.values()]
|
2026-08-23 22:34:25 +03:30
|
|
|
.filter((r) => !SUPERADMIN_OWNED_KEYS.has(r.key))
|
2026-08-19 18:40:07 +03:30
|
|
|
.sort((a, b) => a.key.localeCompare(b.key));
|
|
|
|
|
|
|
|
|
|
const jsonOut = systemRows.map((r) => ({
|
|
|
|
|
key: r.key,
|
2026-08-19 21:47:43 +03:30
|
|
|
value: templateValue(r),
|
2026-08-19 18:40:07 +03:30
|
|
|
title_fa: r.title_fa ?? r.key,
|
|
|
|
|
can_edit: 0,
|
|
|
|
|
is_active: r.is_active ?? 1,
|
|
|
|
|
description: r.description ?? '',
|
|
|
|
|
type: r.type ?? '',
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const tsKeys = `/**
|
2026-08-23 22:34:25 +03:30
|
|
|
* کلیدهای مالکیت سوپرادمین — گروه «دسترسی مشتری به قابلیتها».
|
2026-08-19 18:40:07 +03:30
|
|
|
* تولید خودکار: node scripts/sync-superadmin-setting-template.mjs
|
|
|
|
|
*/
|
|
|
|
|
export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
|
|
|
|
|
${systemRows.map((r) => ` ${JSON.stringify(r.key)},`).join('\n')}
|
|
|
|
|
]);
|
|
|
|
|
|
2026-08-23 22:34:25 +03:30
|
|
|
/**
|
|
|
|
|
* کلیدهایی که سوپرادمین مالکشان نیست (مالک: دیتابیس هر نصب).
|
|
|
|
|
* در هر بوت با purgeEditableFromSuperAdmin از قالب و ردیف همهٔ پروژهها حذف میشوند.
|
|
|
|
|
*/
|
2026-08-19 18:40:07 +03:30
|
|
|
export const DEPLOY_SETTING_EDITABLE_KEYS = new Set<string>([
|
|
|
|
|
${editableRows.map((r) => ` ${JSON.stringify(r.key)},`).join('\n')}
|
|
|
|
|
]);
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
writeFileSync(join(root, 'prisma/setting-templates-from-deploy.json'), `${JSON.stringify(jsonOut, null, 2)}\n`);
|
|
|
|
|
writeFileSync(join(root, 'src/program-settings/deploy-setting-catalog-keys.ts'), `${tsKeys}\n`);
|
|
|
|
|
|
2026-08-19 18:49:08 +03:30
|
|
|
const userTs = `/**
|
2026-08-23 22:34:25 +03:30
|
|
|
* کلیدهای تنظیمات سوپرادمین — فقط گروه «دسترسی مشتری به قابلیتها».
|
2026-08-19 18:49:08 +03:30
|
|
|
* تولید خودکار: controlpanel_server/scripts/sync-superadmin-setting-template.mjs
|
|
|
|
|
*/
|
|
|
|
|
export const SUPERADMIN_SYSTEM_SETTING_KEYS = new Set<string>([
|
|
|
|
|
${systemRows.map((r) => ` ${JSON.stringify(r.key)},`).join('\n')}
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export const SUPERADMIN_EDITABLE_SETTING_KEYS = new Set<string>([
|
|
|
|
|
${editableRows.map((r) => ` ${JSON.stringify(r.key)},`).join('\n')}
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export function isSuperAdminSystemSettingKey(key?: string | null): boolean {
|
|
|
|
|
const k = String(key || '').trim();
|
|
|
|
|
if (!k) return false;
|
|
|
|
|
if (SUPERADMIN_EDITABLE_SETTING_KEYS.has(k)) return false;
|
|
|
|
|
return SUPERADMIN_SYSTEM_SETTING_KEYS.has(k);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
writeFileSync(join(root, '../controlpanel_user/src/config/superAdminSettingKeys.ts'), `${userTs}\n`);
|
|
|
|
|
|
2026-08-23 22:34:25 +03:30
|
|
|
console.log(`superadmin-owned keys: ${systemRows.length}`);
|
|
|
|
|
console.log(`locally owned keys (purged from superadmin): ${editableRows.length}`);
|
|
|
|
|
for (const key of SUPERADMIN_OWNED_KEYS) {
|
2026-08-19 21:47:43 +03:30
|
|
|
const ok = systemRows.some((r) => r.key === key);
|
|
|
|
|
console.log(` ${ok ? 'OK' : 'MISSING'} ${key}`);
|
|
|
|
|
}
|
2026-08-23 22:34:25 +03:30
|
|
|
const mustNot = [
|
|
|
|
|
'price_provider',
|
|
|
|
|
'node_shared_secret',
|
|
|
|
|
'chat_api_key',
|
|
|
|
|
'external_payment_client_secret',
|
|
|
|
|
'wallet_payment_provider',
|
|
|
|
|
'zarin_token',
|
|
|
|
|
'tahesab_token',
|
|
|
|
|
'pargar_password',
|
|
|
|
|
'name_fa',
|
|
|
|
|
'buy_fee',
|
|
|
|
|
];
|
2026-08-19 21:47:43 +03:30
|
|
|
for (const key of mustNot) {
|
|
|
|
|
const bad = systemRows.some((r) => r.key === key);
|
2026-08-23 22:34:25 +03:30
|
|
|
console.log(` ${bad ? 'LEAK' : 'OK'} superadmin must not own ${key}`);
|
2026-08-19 21:47:43 +03:30
|
|
|
}
|