feat(admin): overlay system settings from SuperAdmin program-settings

Fetch /api/public/program-settings with 3-host retry and hide the local
advanced-configuration editor in favor of SuperAdmin.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 16:10:37 +03:30
co-authored by Cursor
parent 7440a53c80
commit 69b6a1c4a9
4 changed files with 135 additions and 39 deletions
+78
View File
@@ -0,0 +1,78 @@
export type SuperAdminSettingRow = {
id?: number
key?: string
value?: string | number | null
title_fa?: string
description?: string
is_active?: boolean | number | string
type?: string
can_edit?: number | boolean | string
}
const SUPERADMIN_BASES = [
'https://adm.montzar.ir',
'https://adm.montech.ir',
'https://adm.montest.ir',
] as const
const SA_RETRY_ROUNDS = 3
const SA_PROGRAM_SETTINGS_PATH = '/api/public/program-settings'
function resolveTenantDomain() {
const host = String(window.location.hostname || '').trim()
if (!host || host === 'localhost' || host === '127.0.0.1') {
return 'k.montzar.ir'
}
return host
}
function parseSettingsPayload(data: unknown): SuperAdminSettingRow[] {
if (Array.isArray(data)) return data as SuperAdminSettingRow[]
if (data && typeof data === 'object') {
const obj = data as Record<string, unknown>
if (Array.isArray(obj.settings)) return obj.settings as SuperAdminSettingRow[]
if (Array.isArray(obj.items)) return obj.items as SuperAdminSettingRow[]
if (Array.isArray(obj.data)) return obj.data as SuperAdminSettingRow[]
}
return []
}
async function fetchFromUrl(url: string, timeout = 5000): Promise<SuperAdminSettingRow[]> {
const controller = new AbortController()
const timer = window.setTimeout(() => controller.abort(), timeout)
try {
const res = await fetch(url, {
method: 'GET',
cache: 'no-store',
signal: controller.signal,
headers: { Accept: 'application/json' },
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const contentType = res.headers.get('content-type') || ''
if (!contentType.includes('application/json')) {
throw new Error('SuperAdmin response is not JSON')
}
return parseSettingsPayload(await res.json())
} finally {
window.clearTimeout(timer)
}
}
/** تنظیمات پیشرفته نسخه از سوپرادمین — فقط GET /api/public/program-settings?domain= */
export async function fetchSuperAdminProgramSettings(): Promise<SuperAdminSettingRow[]> {
const domain = encodeURIComponent(resolveTenantDomain())
let lastError: unknown
for (let round = 1; round <= SA_RETRY_ROUNDS; round++) {
for (const base of SUPERADMIN_BASES) {
try {
return await fetchFromUrl(`${base}${SA_PROGRAM_SETTINGS_PATH}?domain=${domain}`)
} catch (error) {
lastError = error
}
}
}
console.warn('SuperAdmin program-settings failed after retries:', lastError)
return []
}
+37 -21
View File
@@ -18,8 +18,8 @@
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock';
const VERSION_LAST_RELOAD_KEY = 'version_last_reload';
const AUTH_TOKEN_KEY = 'peransa_token';
const VERSION_RETRY_ROUNDS = 5;
const CONFIG_RETRY_ROUNDS = 5;
const VERSION_RETRY_ROUNDS = 3;
const CONFIG_RETRY_ROUNDS = 3;
const CONFIG_TIMEOUT = 12000;
let configTimeoutId;
@@ -37,33 +37,50 @@
return host;
}
function isDevHost() {
const host = String(window.location.hostname || '').trim();
return host === 'localhost' || host === '127.0.0.1';
}
function getVersionUrls() {
const domain = encodeURIComponent(resolveTenantDomain());
return [
const urls = [
'https://adm.montzar.ir/get_version?domain=' + domain,
'https://adm.montech.ir/get_version?domain=' + domain,
'https://adm.montest.ir/get_version?domain=' + domain,
'https://tablo.talasevin.ir/get_version',
'https://abshode-service.montgit.ir/server/get_version',
getProtocol() + '//212.23.201.173:8381/get_version',
getProtocol() + '//91.212.174.137:4040/server/get_version'
'https://adm.montest.ir/get_version?domain=' + domain
];
if (isDevHost()) {
urls.push(
'https://tablo.talasevin.ir/get_version',
'https://abshode-service.montgit.ir/server/get_version',
getProtocol() + '//212.23.201.173:8381/get_version',
getProtocol() + '//91.212.174.137:4040/server/get_version'
);
}
return urls;
}
function getConfigUrls() {
const protocol = getProtocol();
const domain = encodeURIComponent(resolveTenantDomain());
return [
const urls = [
'https://adm.montzar.ir/get_user?domain=' + domain,
'https://adm.montech.ir/get_user?domain=' + domain,
'https://adm.montest.ir/get_user?domain=' + domain,
protocol + '//tablo.talasevin.ir/get_user',
protocol + '//abshode-service.montgit.ir/server/get_user',
protocol + '//212.23.201.173:8381/get_user',
protocol + '//91.212.174.137:4040/server/get_user'
'https://adm.montest.ir/get_user?domain=' + domain
];
if (isDevHost()) {
urls.push(
protocol + '//tablo.talasevin.ir/get_user',
protocol + '//abshode-service.montgit.ir/server/get_user',
protocol + '//212.23.201.173:8381/get_user',
protocol + '//91.212.174.137:4040/server/get_user'
);
}
return urls;
}
function sortUrls(urls, successKey) {
@@ -367,8 +384,9 @@
'<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;direction:rtl;font-family:sans-serif;padding:24px;background:#fff;color:#111827;">' +
'<div style="width:min(420px,100%);text-align:center;border:1px solid rgba(15,23,42,0.08);border-radius:24px;padding:28px 22px;">' +
'<div style="font-size:42px;margin-bottom:12px;">⚠️</div>' +
'<h2 style="margin:0 0 20px;font-size:20px;">خطا در برقراری ارتباط با سرور</h2>' +
'<button type="button" onclick="location.reload()" style="padding:11px 22px;border:0;border-radius:12px;background:#2563eb;color:#fff;cursor:pointer;font-weight:700;">تلاش مجدد</button>' +
'<h2 style="margin:0 0 12px;font-size:20px;">خطا در برقراری ارتباط با سرور</h2>' +
'<p style="margin:0 0 20px;color:rgba(15,23,42,0.62);line-height:2;font-size:14px;">برقراری ارتباط با سرور امکان‌پذیر نیست. لطفاً اتصال اینترنت خود را بررسی کرده و صفحه را تازه‌سازی نمایید.</p>' +
'<button type="button" onclick="location.reload()" style="padding:11px 22px;border:0;border-radius:12px;background:#2563eb;color:#fff;cursor:pointer;font-weight:700;">تازه‌سازی صفحه</button>' +
'</div></div>';
}
@@ -477,9 +495,7 @@
}
}
window.__APP_CONFIG__ = {};
window.__CONFIG_READY__ = true;
finishConfigLoad({}, error);
showServerUnavailable();
}
}
+3 -16
View File
@@ -1,5 +1,4 @@
import type { RouteRecordRaw } from 'vue-router'
import { isDeployAdmin } from '@/utils/deployAdmin'
import { hasSmsPanelAccess } from '@/utils/smsPanelAccess'
import { useUserStore } from '@/store/userStore'
import { bannerEnabledSetting, trackingSystemsEnabledSetting, appNotificationEnabledSetting } from '@/utils/config'
@@ -60,22 +59,10 @@ const Setting: RouteRecordRaw = {
}
},
{
name: 'advancedConfiguration',
name: 'advancedConfigurationRedirect',
path: 'advanced-configuration',
component: () => import('@/pages/menuPages/configuration/index.vue'),
props: { mode: 'system' },
meta: {
deployAdminOnly: true,
icon: 'vuesax-linear:code-circle',
title: 'تنظیمات پیشرفته',
order: 3
},
beforeEnter() {
const userStore = useUserStore()
if (!isDeployAdmin(userStore.userInfo?.mobile)) {
return { name: 'configuration' }
}
}
redirect: { name: 'configuration' },
meta: { hideMenu: true }
},
{
name: 'smsPanelMain',
+17 -2
View File
@@ -332,9 +332,24 @@ async function bootstrapSettingsFromApi(): Promise<void> {
try {
const { getConfiguration } = await import('@/api/configuration')
const { fetchSuperAdminProgramSettings } = await import('@/api/superadminSettings')
const { parseMainWalletProductIds } = await import('@/components/mobile/utils/wallet')
const rows = await getConfiguration()
const list = Array.isArray(rows) ? rows : []
const [localRows, saRows] = await Promise.all([
getConfiguration().catch(() => []),
fetchSuperAdminProgramSettings(),
])
const local = Array.isArray(localRows) ? localRows : []
const merged = [...local]
for (const item of saRows) {
if (!item?.key) continue
const index = merged.findIndex((row) => row.key === item.key)
if (index >= 0) {
merged[index] = { ...merged[index], ...item }
} else {
merged.push(item as (typeof merged)[number])
}
}
const list = merged
const isStoreRow = list.find((item) => item.key === 'is_store')
const mainWalletRow = list.find((item) => item.key === 'main_wallet_products')
const mainMetalRow = list.find((item) => item.key === 'main_metal')