feat(auth): enforce admin permissions in UI

Add can()/router meta, HomePage module filtering, Admins permission
matrix, and hide write actions when the user lacks *:write.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-10 15:30:26 +03:30
co-authored by Cursor
parent 8d0b65f087
commit df83af90e8
15 changed files with 590 additions and 84 deletions
+91 -46
View File
@@ -12,6 +12,7 @@ import {
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import api from '@/api/http'
import { useAuthStore } from '@/stores/auth'
type BalanceLevel = 'healthy' | 'warning' | 'critical' | 'unknown'
@@ -56,6 +57,7 @@ type ExpiringPlugin = {
type ExpiringSupport = ExpiringPlugin
const router = useRouter()
const auth = useAuthStore()
const loading = ref(true)
const tenants = ref<Tenant[]>([])
const smsTenants = ref<Tenant[]>([])
@@ -67,52 +69,95 @@ const hamkarCount = computed(() => tenants.value.filter((t) => t.product === 'ha
const khanegiCount = computed(() => tenants.value.filter((t) => t.product === 'khanegi').length)
const lowestSmsTenants = computed(() => smsTenants.value.slice(0, 5))
const modules = computed(() => [
{
key: 'admins',
className: 'admins',
title: 'مدیران',
desc: 'افزودن و ویرایش مدیران؛ ورود با موبایل و دریافت هشدار SMS',
meta: 'دسترسی',
icon: UserOutlined,
to: { name: 'admins' as const },
},
{
key: 'hamkar',
className: 'hamkar',
title: 'همکار',
desc: 'مدیریت نسخه‌های فروشی پروژه همکار',
meta: `${hamkarCount.value} نسخه`,
icon: TeamOutlined,
to: { name: 'product-versions' as const, params: { product: 'hamkar' } },
},
{
key: 'khanegi',
className: 'khanegi',
title: 'خانگی',
desc: 'مدیریت نسخه‌های فروشی و ظاهر اپ خانگی',
meta: `${khanegiCount.value} نسخه`,
icon: HomeOutlined,
to: { name: 'product-versions' as const, params: { product: 'khanegi' } },
},
])
const canSms = computed(() => auth.can('sms:read'))
const canServers = computed(() => auth.can('servers:read'))
const canPlugins = computed(() => auth.can('plugins:read'))
const canSupport = computed(() => auth.can('support:read'))
const canAdmins = computed(() => auth.can('admins:read'))
const canHamkar = computed(() => auth.can('hamkar:read'))
const canKhanegi = computed(() => auth.can('khanegi:read'))
const modules = computed(() =>
[
{
key: 'admins',
className: 'admins',
title: 'مدیران',
desc: 'افزودن و ویرایش مدیران؛ تعیین سطح دسترسی',
meta: 'دسترسی',
icon: UserOutlined,
to: { name: 'admins' as const },
visible: canAdmins.value,
},
{
key: 'hamkar',
className: 'hamkar',
title: 'همکار',
desc: 'مدیریت نسخه‌های فروشی پروژه همکار',
meta: `${hamkarCount.value} نسخه`,
icon: TeamOutlined,
to: { name: 'product-versions' as const, params: { product: 'hamkar' } },
visible: canHamkar.value,
},
{
key: 'khanegi',
className: 'khanegi',
title: 'خانگی',
desc: 'مدیریت نسخه‌های فروشی و ظاهر اپ خانگی',
meta: `${khanegiCount.value} نسخه`,
icon: HomeOutlined,
to: { name: 'product-versions' as const, params: { product: 'khanegi' } },
visible: canKhanegi.value,
},
].filter((item) => item.visible),
)
async function load() {
loading.value = true
try {
const [tenantsRes, lowestRes, expiringRes, pluginExpiringRes, supportExpiringRes] =
await Promise.all([
api.get('/tenants'),
api.get('/sms/lowest', { params: { limit: 5 } }),
api.get('/servers/expiring', { params: { limit: 5 } }),
api.get('/plugins/expiring', { params: { limit: 5 } }),
api.get('/support/expiring', { params: { limit: 5 } }),
])
tenants.value = tenantsRes.data
smsTenants.value = lowestRes.data
expiringServers.value = expiringRes.data
expiringPlugins.value = pluginExpiringRes.data
expiringSupports.value = supportExpiringRes.data
const tasks: Array<Promise<void>> = []
if (canHamkar.value || canKhanegi.value) {
tasks.push(
api.get('/tenants').then((res) => {
tenants.value = res.data
}),
)
}
if (canSms.value) {
tasks.push(
api.get('/sms/lowest', { params: { limit: 5 } }).then((res) => {
smsTenants.value = res.data
}),
)
}
if (canServers.value) {
tasks.push(
api.get('/servers/expiring', { params: { limit: 5 } }).then((res) => {
expiringServers.value = res.data
}),
)
}
if (canPlugins.value) {
tasks.push(
api.get('/plugins/expiring', { params: { limit: 5 } }).then((res) => {
expiringPlugins.value = res.data
}),
)
}
if (canSupport.value) {
tasks.push(
api.get('/support/expiring', { params: { limit: 5 } }).then((res) => {
expiringSupports.value = res.data
}),
)
}
const results = await Promise.allSettled(tasks)
const failed = results.some((r) => r.status === 'rejected')
if (failed) {
message.error('خطا در دریافت بخشی از آمار داشبورد')
}
} catch (e: any) {
message.error(e?.response?.data?.message || 'خطا در دریافت آمار')
} finally {
@@ -193,7 +238,7 @@ onMounted(load)
<a-spin :spinning="loading">
<div class="dash-modules">
<div class="module-card sms sms-dashboard-card" @click="openSmsPanel()">
<div v-if="canSms" class="module-card sms sms-dashboard-card" @click="openSmsPanel()">
<div>
<div class="icon-wrap">
<MessageOutlined />
@@ -227,7 +272,7 @@ onMounted(load)
</div>
</div>
<div class="module-card server sms-dashboard-card" @click="openServerPanel()">
<div v-if="canServers" class="module-card server sms-dashboard-card" @click="openServerPanel()">
<div>
<div class="icon-wrap">
<CloudServerOutlined />
@@ -266,7 +311,7 @@ onMounted(load)
</div>
</div>
<div class="module-card plugin sms-dashboard-card" @click="openPluginPanel()">
<div v-if="canPlugins" class="module-card plugin sms-dashboard-card" @click="openPluginPanel()">
<div>
<div class="icon-wrap">
<AppstoreAddOutlined />
@@ -300,7 +345,7 @@ onMounted(load)
</div>
</div>
<div class="module-card support sms-dashboard-card" @click="openSupportPanel()">
<div v-if="canSupport" class="module-card support sms-dashboard-card" @click="openSupportPanel()">
<div>
<div class="icon-wrap">
<CustomerServiceOutlined />