#!/usr/bin/env node /** * منبع حقیقت: khaneghi_server/database/sql/deploy_settings_const.sql * خروجی: * - prisma/setting-templates-from-deploy.json (فقط can_edit=0 نهایی) * - src/program-settings/deploy-setting-catalog-keys.ts * - ../controlpanel_user/src/config/superAdminSettingKeys.ts * * قالب value خالی است برای توکن/رمز/شناسه پروژه — هر پروژه در سوپرادمین مقدار خودش را دارد. */ 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 systemRows = [...rows.values()] .filter((r) => r.can_edit === 0) .sort((a, b) => a.key.localeCompare(b.key)); const editableRows = [...rows.values()] .filter((r) => r.can_edit === 1) .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 = `/** * کلیدهای can_edit=0 در deploy_settings_const.sql — فقط همین‌ها در قالب سوپرادمین. * تولید خودکار: node scripts/sync-superadmin-setting-template.mjs */ export const DEPLOY_SETTING_CATALOG_KEYS = new Set([ ${systemRows.map((r) => ` ${JSON.stringify(r.key)},`).join('\n')} ]); /** کلیدهای can_edit=1 — نباید در قالب/جدول settings سوپرادمین باشند */ 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 = `/** * کلیدهای تنظیمات سوپرادمین — هم‌تراز deploy_settings_const.sql * تولید خودکار: 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(`system keys (can_edit=0): ${systemRows.length}`); console.log(`editable keys (can_edit=1): ${editableRows.length}`); const mustHave = [ 'price_provider', 'zaarbox_api_url', 'main_product', 'profit_gold_api_enabled', 'title_pwa', 'title_fav', 'unit', 'main_wallet_products', ]; for (const key of mustHave) { const ok = systemRows.some((r) => r.key === key); console.log(` ${ok ? 'OK' : 'MISSING'} ${key}`); } const mustNot = ['montzar_token', 'montzar_diff', 'name_fa', 'buy_fee']; for (const key of mustNot) { const bad = systemRows.some((r) => r.key === key); console.log(` ${bad ? 'LEAK' : 'OK'} system must not contain ${key}`); }