feat: UI switch for server access kill switch

Add SuperAdmin toggle to cut app/admin access for a sold version with push status feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 16:05:04 +03:30
co-authored by Cursor
parent df83af90e8
commit 4e723c31ea
+92
View File
@@ -54,12 +54,15 @@ const tenant = ref<{
slug: string
nameFa: string
domain: string
serverAccessBlocked?: boolean
} | null>(null)
const servers = ref<ServerRow[]>([])
const selectedId = ref<string | null>(null)
const summary = ref<Summary | null>(null)
const charges = ref<any[]>([])
const planOptions = ref<{ months: number; label: string }[]>([])
const accessBlocking = ref(false)
const serverAccessBlocked = ref(false)
const form = reactive({
isDedicated: false,
@@ -140,6 +143,7 @@ async function loadDetail() {
try {
const { data } = await api.get(`/servers/tenants/${tenantId.value}`)
tenant.value = data.tenant
serverAccessBlocked.value = Boolean(data.tenant?.serverAccessBlocked)
servers.value = data.servers || []
planOptions.value = data.planOptions || []
@@ -272,6 +276,48 @@ async function renewServer() {
watch(() => route.params.tenantId, loadDetail)
onMounted(loadDetail)
async function onServerAccessToggle(checked: boolean) {
if (!canWrite.value || !tenantId.value) return
accessBlocking.value = true
const previous = serverAccessBlocked.value
serverAccessBlocked.value = checked
try {
const { data } = await api.post(`/servers/tenants/${tenantId.value}/server-access`, {
blocked: checked,
})
serverAccessBlocked.value = Boolean(data?.tenant?.serverAccessBlocked)
if (tenant.value) {
tenant.value = {
...tenant.value,
serverAccessBlocked: serverAccessBlocked.value,
}
}
if (data?.push?.ok === false) {
message.warning(
data.push.message ||
'قفل در رجیستری ثبت شد ولی push فوری به سرور نسخه ناموفق بود',
)
} else if (data?.push?.stub) {
message.warning(
data.push.message ||
'قفل ثبت شد؛ remoteApiToken نیست — push فوری انجام نشد',
)
} else {
message.success(
serverAccessBlocked.value
? 'دسترسی اپ/ادمین قطع شد'
: 'دسترسی اپ/ادمین وصل شد',
)
}
} catch (e: any) {
serverAccessBlocked.value = previous
const msg = e?.response?.data?.message
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'تغییر دسترسی ناموفق')
} finally {
accessBlocking.value = false
}
}
</script>
<template>
@@ -292,6 +338,26 @@ onMounted(loadDetail)
<TenantPanelShortcuts :tenant-id="tenantId" current="server" />
<a-spin :spinning="loading">
<section class="form-section access-kill">
<div class="access-kill-row" :class="{ blocked: serverAccessBlocked }">
<div>
<h3>قطع دسترسی اپ و ادمین</h3>
<p class="field-hint">
سرور فیزیکی خاموش نمی‌شود؛ فقط اپ و ادمین این نسخه مثل قطعی سرور از کار می‌افتند.
نسخه‌های هم‌سرور دیگر تحت تأثیر نیستند.
</p>
</div>
<a-switch
:checked="serverAccessBlocked"
:loading="accessBlocking"
:disabled="!canWrite"
checked-children="قطع"
un-checked-children="وصل"
@change="onServerAccessToggle"
/>
</div>
</section>
<section class="form-section server-picker">
<div class="picker-head">
<h3>سرورهای این نسخه</h3>
@@ -465,6 +531,32 @@ onMounted(loadDetail)
</template>
<style scoped>
.access-kill {
margin-bottom: 12px;
}
.access-kill-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 14px 16px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
}
.access-kill-row.blocked {
background: #fef2f2;
border-color: #fecaca;
}
.access-kill-row h3 {
margin: 0 0 6px;
font-size: 1rem;
}
.server-picker {
margin-bottom: 8px;
}