feat: standalone admin access-level dialog and granular hamkar/khanegi permissions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+26
-2
@@ -10,8 +10,14 @@ export const PERMISSIONS = [
|
|||||||
'support:write',
|
'support:write',
|
||||||
'hamkar:read',
|
'hamkar:read',
|
||||||
'hamkar:write',
|
'hamkar:write',
|
||||||
|
'hamkar.ui:read',
|
||||||
|
'hamkar.ui:write',
|
||||||
'khanegi:read',
|
'khanegi:read',
|
||||||
'khanegi:write',
|
'khanegi:write',
|
||||||
|
'khanegi.settings:read',
|
||||||
|
'khanegi.settings:write',
|
||||||
|
'khanegi.ui:read',
|
||||||
|
'khanegi.ui:write',
|
||||||
'others:read',
|
'others:read',
|
||||||
'others:write',
|
'others:write',
|
||||||
'admins:read',
|
'admins:read',
|
||||||
@@ -27,6 +33,8 @@ export type PermissionModule = {
|
|||||||
label: string
|
label: string
|
||||||
read?: PermissionCode
|
read?: PermissionCode
|
||||||
write?: PermissionCode
|
write?: PermissionCode
|
||||||
|
/** Sub-section row of a product (shown indented under its parent). */
|
||||||
|
sub?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PERMISSION_MODULES: PermissionModule[] = [
|
export const PERMISSION_MODULES: PermissionModule[] = [
|
||||||
@@ -34,14 +42,30 @@ export const PERMISSION_MODULES: PermissionModule[] = [
|
|||||||
{ key: 'servers', label: 'سرور', read: 'servers:read', write: 'servers:write' },
|
{ key: 'servers', label: 'سرور', read: 'servers:read', write: 'servers:write' },
|
||||||
{ key: 'plugins', label: 'افزونه', read: 'plugins:read', write: 'plugins:write' },
|
{ key: 'plugins', label: 'افزونه', read: 'plugins:read', write: 'plugins:write' },
|
||||||
{ key: 'support', label: 'پشتیبانی', read: 'support:read', write: 'support:write' },
|
{ key: 'support', label: 'پشتیبانی', read: 'support:read', write: 'support:write' },
|
||||||
{ key: 'hamkar', label: 'همکار', read: 'hamkar:read', write: 'hamkar:write' },
|
{ key: 'hamkar', label: 'همکار — مدیریت', read: 'hamkar:read', write: 'hamkar:write' },
|
||||||
{ key: 'khanegi', label: 'خانگی', read: 'khanegi:read', write: 'khanegi:write' },
|
{ key: 'hamkar.ui', label: 'همکار — ظاهر', read: 'hamkar.ui:read', write: 'hamkar.ui:write', sub: true },
|
||||||
|
{ key: 'khanegi', label: 'خانگی — مدیریت', read: 'khanegi:read', write: 'khanegi:write' },
|
||||||
|
{
|
||||||
|
key: 'khanegi.settings',
|
||||||
|
label: 'خانگی — تنظیمات',
|
||||||
|
read: 'khanegi.settings:read',
|
||||||
|
write: 'khanegi.settings:write',
|
||||||
|
sub: true,
|
||||||
|
},
|
||||||
|
{ key: 'khanegi.ui', label: 'خانگی — ظاهر', read: 'khanegi.ui:read', write: 'khanegi.ui:write', sub: true },
|
||||||
{ key: 'others', label: 'سایتهای دیگر', read: 'others:read', write: 'others:write' },
|
{ key: 'others', label: 'سایتهای دیگر', read: 'others:read', write: 'others:write' },
|
||||||
{ key: 'admins', label: 'مدیران', read: 'admins:read', write: 'admins:write' },
|
{ key: 'admins', label: 'مدیران', read: 'admins:read', write: 'admins:write' },
|
||||||
{ key: 'audit', label: 'لاگ', read: 'audit:read' },
|
{ key: 'audit', label: 'لاگ', read: 'audit:read' },
|
||||||
{ key: 'settings', label: 'تنظیمات حساس', write: 'settings:write' },
|
{ key: 'settings', label: 'تنظیمات حساس', write: 'settings:write' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** Any read access to a product section (list / manage / ui / settings). */
|
||||||
|
export function productAnyReadPermissions(product: string): PermissionCode[] {
|
||||||
|
if (product === 'hamkar') return ['hamkar:read', 'hamkar.ui:read']
|
||||||
|
if (product === 'other') return ['others:read']
|
||||||
|
return ['khanegi:read', 'khanegi.ui:read', 'khanegi.settings:read']
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizePermissionList(codes: string[]): PermissionCode[] {
|
export function normalizePermissionList(codes: string[]): PermissionCode[] {
|
||||||
const set = new Set<PermissionCode>()
|
const set = new Set<PermissionCode>()
|
||||||
for (const raw of codes) {
|
for (const raw of codes) {
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import {
|
||||||
|
PERMISSION_MODULES,
|
||||||
|
normalizePermissionList,
|
||||||
|
type PermissionCode,
|
||||||
|
type PermissionModule,
|
||||||
|
} from '@/auth/permissions'
|
||||||
|
|
||||||
|
const fullAccess = defineModel<boolean>('fullAccess', { required: true })
|
||||||
|
const permissions = defineModel<PermissionCode[]>('permissions', { required: true })
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const canGrantFullAccess = computed(() => auth.isFullAccess)
|
||||||
|
const permissionSet = computed(() => new Set(permissions.value))
|
||||||
|
|
||||||
|
function canAssignCode(code: PermissionCode) {
|
||||||
|
if (canGrantFullAccess.value) return true
|
||||||
|
return auth.can(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChecked(code?: PermissionCode) {
|
||||||
|
if (!code) return false
|
||||||
|
return permissionSet.value.has(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPermission(code: PermissionCode | undefined, checked: boolean) {
|
||||||
|
if (!code) return
|
||||||
|
if (!canAssignCode(code)) return
|
||||||
|
const next = new Set(permissions.value)
|
||||||
|
if (checked) {
|
||||||
|
next.add(code)
|
||||||
|
if (code.endsWith(':write')) {
|
||||||
|
const read = code.replace(/:write$/, ':read') as PermissionCode
|
||||||
|
if (canAssignCode(read)) next.add(read)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next.delete(code)
|
||||||
|
if (code.endsWith(':read')) {
|
||||||
|
const write = code.replace(/:read$/, ':write') as PermissionCode
|
||||||
|
next.delete(write)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
permissions.value = normalizePermissionList([...next])
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReadChange(mod: PermissionModule, checked: boolean) {
|
||||||
|
setPermission(mod.read, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWriteChange(mod: PermissionModule, checked: boolean) {
|
||||||
|
setPermission(mod.write, checked)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-form-item v-if="canGrantFullAccess" label="دسترسی کامل (super)">
|
||||||
|
<a-switch v-model:checked="fullAccess" />
|
||||||
|
<div class="field-hint">با روشن بودن، همه ماژولها بدون محدودیت در دسترس است.</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div v-if="!fullAccess" class="perm-matrix">
|
||||||
|
<div class="perm-matrix-head">
|
||||||
|
<span>ماژول</span>
|
||||||
|
<span>خواندن</span>
|
||||||
|
<span>نوشتن</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="mod in PERMISSION_MODULES"
|
||||||
|
:key="mod.key"
|
||||||
|
class="perm-matrix-row"
|
||||||
|
:class="{ 'is-sub': mod.sub }"
|
||||||
|
>
|
||||||
|
<span class="mod-label">{{ mod.label }}</span>
|
||||||
|
<span>
|
||||||
|
<a-checkbox
|
||||||
|
v-if="mod.read"
|
||||||
|
:checked="isChecked(mod.read)"
|
||||||
|
:disabled="!canAssignCode(mod.read)"
|
||||||
|
@change="(e: any) => onReadChange(mod, e.target.checked)"
|
||||||
|
/>
|
||||||
|
<span v-else class="dash">—</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<a-checkbox
|
||||||
|
v-if="mod.write"
|
||||||
|
:checked="isChecked(mod.write)"
|
||||||
|
:disabled="!canAssignCode(mod.write)"
|
||||||
|
@change="(e: any) => onWriteChange(mod, e.target.checked)"
|
||||||
|
/>
|
||||||
|
<span v-else class="dash">—</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.field-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-matrix {
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid #e8eef5;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-matrix-head,
|
||||||
|
.perm-matrix-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 72px 72px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-matrix-head {
|
||||||
|
background: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-matrix-row + .perm-matrix-row {
|
||||||
|
border-top: 1px solid #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-matrix-row.is-sub {
|
||||||
|
background: #fbfcfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-matrix-row.is-sub .mod-label {
|
||||||
|
padding-inline-start: 18px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-label {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -11,6 +12,7 @@ const props = withDefaults(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
function go(name: string, tab: 'identity' | 'ui' | 'settings') {
|
function go(name: string, tab: 'identity' | 'ui' | 'settings') {
|
||||||
if (tab === props.active) return
|
if (tab === props.active) return
|
||||||
@@ -21,6 +23,7 @@ function go(name: string, tab: 'identity' | 'ui' | 'settings') {
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-nav">
|
<div class="page-nav">
|
||||||
<a-button
|
<a-button
|
||||||
|
v-if="auth.can('khanegi:read')"
|
||||||
:type="active === 'identity' ? 'primary' : 'default'"
|
:type="active === 'identity' ? 'primary' : 'default'"
|
||||||
ghost
|
ghost
|
||||||
@click="go('version-edit', 'identity')"
|
@click="go('version-edit', 'identity')"
|
||||||
@@ -28,6 +31,7 @@ function go(name: string, tab: 'identity' | 'ui' | 'settings') {
|
|||||||
هویت نسخه
|
هویت نسخه
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
|
v-if="auth.can('khanegi.ui:read')"
|
||||||
:type="active === 'ui' ? 'primary' : 'default'"
|
:type="active === 'ui' ? 'primary' : 'default'"
|
||||||
ghost
|
ghost
|
||||||
@click="go(uiRouteName, 'ui')"
|
@click="go(uiRouteName, 'ui')"
|
||||||
@@ -35,6 +39,7 @@ function go(name: string, tab: 'identity' | 'ui' | 'settings') {
|
|||||||
ظاهر اپ
|
ظاهر اپ
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
|
v-if="auth.can('khanegi.settings:read')"
|
||||||
:type="active === 'settings' ? 'primary' : 'default'"
|
:type="active === 'settings' ? 'primary' : 'default'"
|
||||||
ghost
|
ghost
|
||||||
@click="go('version-program-settings', 'settings')"
|
@click="go('version-program-settings', 'settings')"
|
||||||
|
|||||||
+76
-133
@@ -8,9 +8,11 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
MessageOutlined,
|
MessageOutlined,
|
||||||
|
SafetyCertificateOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import api from '@/api/http'
|
import api from '@/api/http'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import AdminPermissionEditor from '@/components/AdminPermissionEditor.vue'
|
||||||
import {
|
import {
|
||||||
PERMISSION_MODULES,
|
PERMISSION_MODULES,
|
||||||
normalizePermissionList,
|
normalizePermissionList,
|
||||||
@@ -53,50 +55,22 @@ const form = reactive({
|
|||||||
|
|
||||||
const modalTitle = computed(() => (editingId.value ? 'ویرایش مدیر' : 'افزودن مدیر'))
|
const modalTitle = computed(() => (editingId.value ? 'ویرایش مدیر' : 'افزودن مدیر'))
|
||||||
|
|
||||||
const permissionSet = computed(() => new Set(form.permissions))
|
const permOpen = ref(false)
|
||||||
|
const permSaving = ref(false)
|
||||||
|
const permTarget = ref<Admin | null>(null)
|
||||||
|
const permForm = reactive({
|
||||||
|
fullAccess: false,
|
||||||
|
permissions: [] as PermissionCode[],
|
||||||
|
})
|
||||||
|
|
||||||
|
function isFull(item: Admin) {
|
||||||
|
return item.role === 'super_admin' || Boolean(item.isFullAccess)
|
||||||
|
}
|
||||||
|
|
||||||
function fullName(item: Admin) {
|
function fullName(item: Admin) {
|
||||||
return item.name || `${item.firstName} ${item.lastName}`.trim()
|
return item.name || `${item.firstName} ${item.lastName}`.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAssignCode(code: PermissionCode) {
|
|
||||||
if (canGrantFullAccess.value) return true
|
|
||||||
return auth.can(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isChecked(code?: PermissionCode) {
|
|
||||||
if (!code) return false
|
|
||||||
return permissionSet.value.has(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPermission(code: PermissionCode | undefined, checked: boolean) {
|
|
||||||
if (!code) return
|
|
||||||
if (!canAssignCode(code)) return
|
|
||||||
const next = new Set(form.permissions)
|
|
||||||
if (checked) {
|
|
||||||
next.add(code)
|
|
||||||
if (code.endsWith(':write')) {
|
|
||||||
const read = code.replace(/:write$/, ':read') as PermissionCode
|
|
||||||
if (canAssignCode(read)) next.add(read)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next.delete(code)
|
|
||||||
if (code.endsWith(':read')) {
|
|
||||||
const write = code.replace(/:read$/, ':write') as PermissionCode
|
|
||||||
next.delete(write)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
form.permissions = normalizePermissionList([...next])
|
|
||||||
}
|
|
||||||
|
|
||||||
function onReadChange(mod: (typeof PERMISSION_MODULES)[number], checked: boolean) {
|
|
||||||
setPermission(mod.read, checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWriteChange(mod: (typeof PERMISSION_MODULES)[number], checked: boolean) {
|
|
||||||
setPermission(mod.write, checked)
|
|
||||||
}
|
|
||||||
|
|
||||||
function permissionSummary(item: Admin) {
|
function permissionSummary(item: Admin) {
|
||||||
if (item.role === 'super_admin' || item.isFullAccess) return 'دسترسی کامل'
|
if (item.role === 'super_admin' || item.isFullAccess) return 'دسترسی کامل'
|
||||||
const list = normalizePermissionList(item.permissions || [])
|
const list = normalizePermissionList(item.permissions || [])
|
||||||
@@ -149,11 +123,43 @@ function openEdit(item: Admin) {
|
|||||||
form.mobile = item.mobile
|
form.mobile = item.mobile
|
||||||
form.password = ''
|
form.password = ''
|
||||||
form.isActive = item.isActive
|
form.isActive = item.isActive
|
||||||
form.fullAccess = item.role === 'super_admin' || Boolean(item.isFullAccess)
|
modalOpen.value = true
|
||||||
form.permissions = form.fullAccess
|
}
|
||||||
|
|
||||||
|
function openPermissions(item: Admin) {
|
||||||
|
if (!canWrite.value) return
|
||||||
|
if (isFull(item) && !canGrantFullAccess.value) {
|
||||||
|
message.warning('فقط مدیر با دسترسی کامل میتواند سطح دسترسی این مدیر را تغییر دهد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
permTarget.value = item
|
||||||
|
permForm.fullAccess = isFull(item)
|
||||||
|
permForm.permissions = permForm.fullAccess
|
||||||
? []
|
? []
|
||||||
: normalizePermissionList(item.permissions || [])
|
: normalizePermissionList(item.permissions || [])
|
||||||
modalOpen.value = true
|
permOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePermissions() {
|
||||||
|
const target = permTarget.value
|
||||||
|
if (!target || !canWrite.value) return
|
||||||
|
const role = permForm.fullAccess && canGrantFullAccess.value ? 'super_admin' : 'admin'
|
||||||
|
permSaving.value = true
|
||||||
|
try {
|
||||||
|
await api.patch(`/admins/${target.id}`, {
|
||||||
|
role,
|
||||||
|
permissions: role === 'super_admin' ? [] : normalizePermissionList(permForm.permissions),
|
||||||
|
})
|
||||||
|
message.success('سطح دسترسی ذخیره شد')
|
||||||
|
permOpen.value = false
|
||||||
|
if (target.id === auth.user?.id) await auth.fetchMe()
|
||||||
|
await load()
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e?.response?.data?.message
|
||||||
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ذخیره ناموفق')
|
||||||
|
} finally {
|
||||||
|
permSaving.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@@ -171,20 +177,6 @@ async function save() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const editingWasFull =
|
|
||||||
Boolean(editingId.value) &&
|
|
||||||
rows.value.some(
|
|
||||||
(r) =>
|
|
||||||
r.id === editingId.value &&
|
|
||||||
(r.role === 'super_admin' || Boolean(r.isFullAccess)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const role =
|
|
||||||
form.fullAccess && (canGrantFullAccess.value || editingWasFull)
|
|
||||||
? 'super_admin'
|
|
||||||
: 'admin'
|
|
||||||
const permissions = role === 'super_admin' ? [] : normalizePermissionList(form.permissions)
|
|
||||||
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
if (editingId.value) {
|
if (editingId.value) {
|
||||||
@@ -195,11 +187,6 @@ async function save() {
|
|||||||
isActive: form.isActive,
|
isActive: form.isActive,
|
||||||
}
|
}
|
||||||
if (form.password) payload.password = form.password
|
if (form.password) payload.password = form.password
|
||||||
// Limited editors must not re-assign role/permissions on a full-access user
|
|
||||||
if (!(editingWasFull && !canGrantFullAccess.value)) {
|
|
||||||
payload.role = role
|
|
||||||
payload.permissions = permissions
|
|
||||||
}
|
|
||||||
await api.patch(`/admins/${editingId.value}`, payload)
|
await api.patch(`/admins/${editingId.value}`, payload)
|
||||||
message.success('مدیر بهروزرسانی شد')
|
message.success('مدیر بهروزرسانی شد')
|
||||||
} else {
|
} else {
|
||||||
@@ -304,6 +291,12 @@ onMounted(load)
|
|||||||
<a-button @click="testSms(item)">
|
<a-button @click="testSms(item)">
|
||||||
<MessageOutlined /> تست SMS
|
<MessageOutlined /> تست SMS
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
:disabled="isFull(item) && !canGrantFullAccess"
|
||||||
|
@click="openPermissions(item)"
|
||||||
|
>
|
||||||
|
<SafetyCertificateOutlined /> سطح دسترسی
|
||||||
|
</a-button>
|
||||||
<a-button type="primary" ghost @click="openEdit(item)">
|
<a-button type="primary" ghost @click="openEdit(item)">
|
||||||
<EditOutlined /> ویرایش
|
<EditOutlined /> ویرایش
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -343,39 +336,29 @@ onMounted(load)
|
|||||||
<a-switch v-model:checked="form.isActive" />
|
<a-switch v-model:checked="form.isActive" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="canGrantFullAccess" label="دسترسی کامل (super)">
|
<AdminPermissionEditor
|
||||||
<a-switch v-model:checked="form.fullAccess" />
|
v-if="!editingId"
|
||||||
<div class="field-hint">با روشن بودن، همه ماژولها بدون محدودیت در دسترس است.</div>
|
v-model:full-access="form.fullAccess"
|
||||||
</a-form-item>
|
v-model:permissions="form.permissions"
|
||||||
|
/>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
<div v-if="!form.fullAccess" class="perm-matrix">
|
<a-modal
|
||||||
<div class="perm-matrix-head">
|
v-model:open="permOpen"
|
||||||
<span>ماژول</span>
|
:title="permTarget ? `سطح دسترسی — ${fullName(permTarget)}` : 'سطح دسترسی'"
|
||||||
<span>خواندن</span>
|
:confirm-loading="permSaving"
|
||||||
<span>نوشتن</span>
|
ok-text="ذخیره"
|
||||||
</div>
|
cancel-text="انصراف"
|
||||||
<div v-for="mod in PERMISSION_MODULES" :key="mod.key" class="perm-matrix-row">
|
width="720px"
|
||||||
<span class="mod-label">{{ mod.label }}</span>
|
wrap-class-name="sa-modal-wrap"
|
||||||
<span>
|
@ok="savePermissions"
|
||||||
<a-checkbox
|
>
|
||||||
v-if="mod.read"
|
<a-form layout="vertical" style="margin-top: 12px">
|
||||||
:checked="isChecked(mod.read)"
|
<AdminPermissionEditor
|
||||||
:disabled="!canAssignCode(mod.read)"
|
v-model:full-access="permForm.fullAccess"
|
||||||
@change="(e: any) => onReadChange(mod, e.target.checked)"
|
v-model:permissions="permForm.permissions"
|
||||||
/>
|
/>
|
||||||
<span v-else class="dash">—</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<a-checkbox
|
|
||||||
v-if="mod.write"
|
|
||||||
:checked="isChecked(mod.write)"
|
|
||||||
:disabled="!canAssignCode(mod.write)"
|
|
||||||
@change="(e: any) => onWriteChange(mod, e.target.checked)"
|
|
||||||
/>
|
|
||||||
<span v-else class="dash">—</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,44 +406,4 @@ onMounted(load)
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-hint {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perm-matrix {
|
|
||||||
margin-top: 8px;
|
|
||||||
border: 1px solid #e8eef5;
|
|
||||||
border-radius: 14px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perm-matrix-head,
|
|
||||||
.perm-matrix-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 72px 72px;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perm-matrix-head {
|
|
||||||
background: #f8fafc;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perm-matrix-row + .perm-matrix-row {
|
|
||||||
border-top: 1px solid #eef2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mod-label {
|
|
||||||
font-size: 0.92rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dash {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const canWrite = computed(() => auth.can('hamkar:write'))
|
const canWrite = computed(() => auth.can('hamkar.ui:write'))
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const resetting = ref(false)
|
const resetting = ref(false)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import api from '@/api/http'
|
import api from '@/api/http'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { productAnyReadPermissions } from '@/auth/permissions'
|
||||||
import { productLabel, type Product } from '@/utils/product'
|
import { productLabel, type Product } from '@/utils/product'
|
||||||
|
|
||||||
type BalanceLevel = 'healthy' | 'warning' | 'critical' | 'unknown'
|
type BalanceLevel = 'healthy' | 'warning' | 'critical' | 'unknown'
|
||||||
@@ -78,8 +79,8 @@ const canServers = computed(() => auth.can('servers:read'))
|
|||||||
const canPlugins = computed(() => auth.can('plugins:read'))
|
const canPlugins = computed(() => auth.can('plugins:read'))
|
||||||
const canSupport = computed(() => auth.can('support:read'))
|
const canSupport = computed(() => auth.can('support:read'))
|
||||||
const canAdmins = computed(() => auth.can('admins:read'))
|
const canAdmins = computed(() => auth.can('admins:read'))
|
||||||
const canHamkar = computed(() => auth.can('hamkar:read'))
|
const canHamkar = computed(() => auth.canAny(productAnyReadPermissions('hamkar')))
|
||||||
const canKhanegi = computed(() => auth.can('khanegi:read'))
|
const canKhanegi = computed(() => auth.canAny(productAnyReadPermissions('khanegi')))
|
||||||
const canOthers = computed(() => auth.can('others:read'))
|
const canOthers = computed(() => auth.can('others:read'))
|
||||||
|
|
||||||
const modules = computed(() =>
|
const modules = computed(() =>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const canWrite = computed(() => auth.can('khanegi:write'))
|
const canWrite = computed(() => auth.can('khanegi.ui:write'))
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const switching = ref(false)
|
const switching = ref(false)
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ const canWrite = computed(() => {
|
|||||||
if (product.value === 'other') return auth.can('others:write')
|
if (product.value === 'other') return auth.can('others:write')
|
||||||
return auth.can('khanegi:write')
|
return auth.can('khanegi:write')
|
||||||
})
|
})
|
||||||
|
const canManage = computed(() => {
|
||||||
|
if (product.value === 'hamkar') return auth.can('hamkar:read')
|
||||||
|
if (product.value === 'other') return auth.can('others:read')
|
||||||
|
return auth.can('khanegi:read')
|
||||||
|
})
|
||||||
|
const canAppearance = computed(() =>
|
||||||
|
auth.can(product.value === 'hamkar' ? 'hamkar.ui:read' : 'khanegi.ui:read'),
|
||||||
|
)
|
||||||
|
const canSettings = computed(() => product.value === 'khanegi' && auth.can('khanegi.settings:read'))
|
||||||
|
const canSettingsWrite = computed(() => auth.can('khanegi.settings:write'))
|
||||||
|
|
||||||
const title = computed(() => productLabel(product.value))
|
const title = computed(() => productLabel(product.value))
|
||||||
const subtitle = computed(() => {
|
const subtitle = computed(() => {
|
||||||
@@ -170,8 +180,8 @@ onMounted(load)
|
|||||||
<a-button v-if="canWrite" type="primary" @click="addVersion">
|
<a-button v-if="canWrite" type="primary" @click="addVersion">
|
||||||
<PlusOutlined /> {{ isOther ? 'سایت جدید' : 'نسخه جدید' }}
|
<PlusOutlined /> {{ isOther ? 'سایت جدید' : 'نسخه جدید' }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<SettingsCatalogPanel v-if="product === 'khanegi'" :can-write="canWrite" />
|
<SettingsCatalogPanel v-if="canSettings" :can-write="canSettingsWrite" />
|
||||||
<KhanegiDepositPanel v-if="product === 'khanegi'" :can-write="canWrite" />
|
<KhanegiDepositPanel v-if="product === 'khanegi' && canManage" :can-write="canWrite" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
@@ -219,12 +229,12 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-actions">
|
<div class="version-actions">
|
||||||
<a-button size="small" type="primary" ghost @click="edit(item.id)">
|
<a-button v-if="canManage" size="small" type="primary" ghost @click="edit(item.id)">
|
||||||
<template #icon><SettingOutlined /></template>
|
<template #icon><SettingOutlined /></template>
|
||||||
مدیریت
|
مدیریت
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="product === 'khanegi'"
|
v-if="canSettings"
|
||||||
size="small"
|
size="small"
|
||||||
@click="goProgramSettings(item.id)"
|
@click="goProgramSettings(item.id)"
|
||||||
>
|
>
|
||||||
@@ -242,7 +252,7 @@ onMounted(load)
|
|||||||
{{ link.label }}
|
{{ link.label }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
<a-button v-else size="small" @click="goAppearance(item)">
|
<a-button v-else-if="canAppearance" size="small" @click="goAppearance(item)">
|
||||||
<template #icon><SkinOutlined /></template>
|
<template #icon><SkinOutlined /></template>
|
||||||
ظاهر
|
ظاهر
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type TenantMeta = {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const canWrite = computed(() => auth.can('khanegi:write'))
|
const canWrite = computed(() => auth.can('khanegi.settings:write'))
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const importing = ref(false)
|
const importing = ref(false)
|
||||||
const purging = ref(false)
|
const purging = ref(false)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const canWrite = computed(() => auth.can('khanegi:write'))
|
const canWrite = computed(() => auth.can('khanegi.ui:write'))
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const resetting = ref(false)
|
const resetting = ref(false)
|
||||||
|
|||||||
+14
-15
@@ -1,7 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import type { PermissionCode } from '@/auth/permissions'
|
import { productAnyReadPermissions, type PermissionCode } from '@/auth/permissions'
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface RouteMeta {
|
interface RouteMeta {
|
||||||
@@ -92,7 +92,14 @@ const router = createRouter({
|
|||||||
name: 'product-versions',
|
name: 'product-versions',
|
||||||
component: () => import('@/pages/ProductVersionsPage.vue'),
|
component: () => import('@/pages/ProductVersionsPage.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
permissions: ['hamkar:read', 'khanegi:read', 'others:read'],
|
permissions: [
|
||||||
|
'hamkar:read',
|
||||||
|
'hamkar.ui:read',
|
||||||
|
'khanegi:read',
|
||||||
|
'khanegi.ui:read',
|
||||||
|
'khanegi.settings:read',
|
||||||
|
'others:read',
|
||||||
|
],
|
||||||
permissionMode: 'any',
|
permissionMode: 'any',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -118,25 +125,25 @@ const router = createRouter({
|
|||||||
path: 'versions/:id/ui',
|
path: 'versions/:id/ui',
|
||||||
name: 'version-ui',
|
name: 'version-ui',
|
||||||
component: () => import('@/pages/UiConfigPage.vue'),
|
component: () => import('@/pages/UiConfigPage.vue'),
|
||||||
meta: { permissions: ['khanegi:read'] },
|
meta: { permissions: ['khanegi.ui:read'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'versions/:id/ui-modular',
|
path: 'versions/:id/ui-modular',
|
||||||
name: 'version-ui-modular',
|
name: 'version-ui-modular',
|
||||||
component: () => import('@/pages/KhanegiModularUiConfigPage.vue'),
|
component: () => import('@/pages/KhanegiModularUiConfigPage.vue'),
|
||||||
meta: { permissions: ['khanegi:read'] },
|
meta: { permissions: ['khanegi.ui:read'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'versions/:id/settings',
|
path: 'versions/:id/settings',
|
||||||
name: 'version-program-settings',
|
name: 'version-program-settings',
|
||||||
component: () => import('@/pages/ProgramSettingsPage.vue'),
|
component: () => import('@/pages/ProgramSettingsPage.vue'),
|
||||||
meta: { permissions: ['khanegi:read'] },
|
meta: { permissions: ['khanegi.settings:read'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'versions/:id/ui-hamkar',
|
path: 'versions/:id/ui-hamkar',
|
||||||
name: 'version-ui-hamkar',
|
name: 'version-ui-hamkar',
|
||||||
component: () => import('@/pages/HamkarUiConfigPage.vue'),
|
component: () => import('@/pages/HamkarUiConfigPage.vue'),
|
||||||
meta: { permissions: ['hamkar:read'] },
|
meta: { permissions: ['hamkar.ui:read'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'audit',
|
path: 'audit',
|
||||||
@@ -178,15 +185,7 @@ router.beforeEach(async (to) => {
|
|||||||
// product-versions: also enforce exact product from params
|
// product-versions: also enforce exact product from params
|
||||||
if (to.name === 'product-versions') {
|
if (to.name === 'product-versions') {
|
||||||
const product = String(to.params.product || '')
|
const product = String(to.params.product || '')
|
||||||
if (product === 'hamkar' && !auth.can('hamkar:read')) {
|
if (!auth.canAny(productAnyReadPermissions(product))) {
|
||||||
message.warning('به این بخش دسترسی ندارید')
|
|
||||||
return { name: 'home' }
|
|
||||||
}
|
|
||||||
if (product === 'khanegi' && !auth.can('khanegi:read')) {
|
|
||||||
message.warning('به این بخش دسترسی ندارید')
|
|
||||||
return { name: 'home' }
|
|
||||||
}
|
|
||||||
if (product === 'other' && !auth.can('others:read')) {
|
|
||||||
message.warning('به این بخش دسترسی ندارید')
|
message.warning('به این بخش دسترسی ندارید')
|
||||||
return { name: 'home' }
|
return { name: 'home' }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user