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:
+91
-46
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user