#!/usr/bin/env node /** * منبع حقیقت: khaneghi_server/database/sql/deploy_settings_const.sql * خروجی: * - prisma/setting-templates-from-deploy.json (فقط کلیدهای مالکیت سوپرادمین) * - src/program-settings/deploy-setting-catalog-keys.ts * - ../controlpanel_user/src/config/superAdminSettingKeys.ts * * مالکیت سوپرادمین دیگر با can_edit تعیین نمی‌شود: فقط کلیدهای SUPERADMIN_OWNED_KEYS * (تب «دسترسی مشتری به قابلیت‌ها») در سوپرادمین می‌مانند و بقیه مالک دیتابیس هر نصب‌اند. * کلیدهای لوکال در جدول settings همان can_edit=0 خودشان را نگه می‌دارند تا در تب * «تنظیمات پیشرفته» پنل خانگی بمانند؛ این‌جا فقط از قالب سوپرادمین بیرون می‌روند. */ 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} */ const rows = new Map(); function unquote(s) { return String(s ?? '') .trim() .replace(/^'/, '') .replace(/'$/, '') .replace(/\\'/g, "'") .replace(/\\n/g, '\n'); } 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; } function applyCanEdit(block, value) { const keys = [...block.matchAll(/'([^']+)'/g)].map((x) => x[1]); for (const key of keys) { const row = ensureRow(key); if (row) row.can_edit = value; } } 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 — منبع حقیقت نهایی) 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, )) { const row = ensureRow(m[2]); 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; } } 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; } /** تنها کلیدهایی که سوپرادمین مالکشان می‌ماند — گروه «دسترسی مشتری به قابلیت‌ها» */ const SUPERADMIN_OWNED_KEYS = new Set([ 'customer_direct_sms', 'customer_special_pricing', 'enable_automation', 'enable_conditional_order', 'enable_customer_level_profit', 'enable_gold_credit', 'enable_product_funds', 'enable_remittance', 'group_sms', 'has_charts', 'has_landing', 'has_page_earn_money', 'has_page_gift_card', 'has_page_notification', 'has_page_price_trigger', 'jibit_enabled', 'permision__enabled', 'show_to_users_chat', ]); /** متادیتای کلیدهای SA که INSERTشان در SQL کامنت شده — برای قالب سوپرادمین */ const SUPERADMIN_OWNED_META = { enable_automation: { value: 'false', title_fa: 'اتوماسیون', description: 'روشن: منوی اتوماسیون در پنل ادمین و اجرای موتور فعال است. خاموش: منو مخفی است و API/کران اجرا نمی‌شود. فقط از سوپرادمین قابل تغییر است.', type: 'switch', }, enable_gold_credit: { value: 'false', title_fa: 'اعتبار طلایی', description: 'روشن: زیرمنوی اعتبار در مشتریان و صفحه اعتبار در اپ مشتری. خاموش: غیرفعال. فقط از سوپرادمین.', type: 'switch', }, has_landing: { value: 'false', title_fa: 'لندینگ وبسایت', description: 'روشن: منوی تنظیمات وبسایت در ادمین و تنظیمات لندینگ برای اپ کاربر فعال است. خاموش: منو مخفی است.', type: 'switch', }, has_charts: { value: 'false', title_fa: 'نمودارها', description: 'روشن: بخش نمودارها در اپلیکیشن فعال است. خاموش: نمودارها غیرفعال هستند.', type: 'switch', }, enable_remittance: { value: 'false', title_fa: 'حواله', description: 'روشن: امکان انتقال وجه یا محصول از کیف پول به سایر شماره‌ها توسط کاربران فعال است. خاموش: این امکان غیرفعال است.', type: 'switch', }, }; for (const [key, meta] of Object.entries(SUPERADMIN_OWNED_META)) { const row = ensureRow(key); if (!row) continue; row.can_edit = 0; row.is_active = 1; if (meta.value !== undefined) row.value = meta.value; if (meta.title_fa) row.title_fa = meta.title_fa; if (meta.description) row.description = meta.description; if (meta.type) row.type = meta.type; } const systemRows = [...rows.values()] .filter((r) => r.can_edit === 0 && SUPERADMIN_OWNED_KEYS.has(r.key)) .sort((a, b) => a.key.localeCompare(b.key)); const editableRows = [...rows.values()] .filter((r) => !SUPERADMIN_OWNED_KEYS.has(r.key)) .sort((a, b) => a.key.localeCompare(b.key)); const jsonOut = systemRows.map((r) => ({ key: r.key, value: templateValue(r), title_fa: r.title_fa ?? r.key, can_edit: 0, is_active: r.is_active ?? 1, description: r.description ?? '', type: r.type ?? '', })); const tsKeys = `/** * کلیدهای مالکیت سوپرادمین — گروه «دسترسی مشتری به قابلیت‌ها». * تولید خودکار: node scripts/sync-superadmin-setting-template.mjs */ export const DEPLOY_SETTING_CATALOG_KEYS = new Set([ ${systemRows.map((r) => ` ${JSON.stringify(r.key)},`).join('\n')} ]); /** * کلیدهایی که سوپرادمین مالکشان نیست (مالک: دیتابیس هر نصب). * در هر بوت از قالب حذف می‌شوند؛ از ردیف هر پروژه فقط با دکمهٔ صفحهٔ تنظیمات همان پروژه. */ export const DEPLOY_SETTING_EDITABLE_KEYS = new Set([ ${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`); const userTs = `/** * کلیدهای تنظیمات سوپرادمین — فقط گروه «دسترسی مشتری به قابلیت‌ها». * تولید خودکار: controlpanel_server/scripts/sync-superadmin-setting-template.mjs */ export const SUPERADMIN_SYSTEM_SETTING_KEYS = new Set([ ${systemRows.map((r) => ` ${JSON.stringify(r.key)},`).join('\n')} ]); export const SUPERADMIN_EDITABLE_SETTING_KEYS = new Set([ ${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`); console.log(`superadmin-owned keys: ${systemRows.length}`); console.log(`locally owned keys (purged from superadmin): ${editableRows.length}`); for (const key of SUPERADMIN_OWNED_KEYS) { const ok = systemRows.some((r) => r.key === key); console.log(` ${ok ? 'OK' : 'MISSING'} ${key}`); } 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', ]; for (const key of mustNot) { const bad = systemRows.some((r) => r.key === key); console.log(` ${bad ? 'LEAK' : 'OK'} superadmin must not own ${key}`); }