feat(acl): deep data access by section for roles and users
Role default + user override with OR rules; covers customers, leads, sales, activities, notes, routines, projects, and tickets.
This commit is contained in:
@@ -12,6 +12,7 @@ import UiIcon from '@/components/UiIcon.vue';
|
||||
import { UiDropdownMenu, UiModal } from '@/components/ui';
|
||||
import {
|
||||
ROLE_PERMISSION_ITEMS,
|
||||
type DataAccessPolicy,
|
||||
type InviteUserRequest,
|
||||
type OrganizationDto,
|
||||
type OrgModuleKey,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
toFaDigits,
|
||||
} from '@/lib/jalali';
|
||||
import TeamsSettingsPanel from '@/components/TeamsSettingsPanel.vue';
|
||||
import DataAccessPolicyPanel from '@/components/DataAccessPolicyPanel.vue';
|
||||
|
||||
type TabKey = 'users' | 'roles' | 'access' | 'teams';
|
||||
type RoleColor = 'teal' | 'violet' | 'rose' | 'sky' | 'amber' | 'slate' | 'pink';
|
||||
@@ -89,6 +91,8 @@ const orgName = ref('');
|
||||
const businessField = ref('');
|
||||
const orgSaving = ref(false);
|
||||
const accessSaving = ref(false);
|
||||
const roleFormDataAccess = ref<DataAccessPolicy>({});
|
||||
const focusDataAccess = ref<DataAccessPolicy>({});
|
||||
|
||||
const tab = ref<TabKey>('users');
|
||||
const query = ref('');
|
||||
@@ -583,6 +587,7 @@ function openCreateRole(parentId?: string | null) {
|
||||
roleFormPerms.value = parent
|
||||
? [...parent.permissions]
|
||||
: visibleRolePermissions.value.map((p) => p.key);
|
||||
roleFormDataAccess.value = clonePolicy(parent?.dataAccessPolicy);
|
||||
roleFormOpen.value = true;
|
||||
}
|
||||
|
||||
@@ -602,6 +607,7 @@ function openEditRole() {
|
||||
: 'teal') as RoleColor;
|
||||
roleFormParentId.value = role.systemKey ? '' : (role.parentId ?? '');
|
||||
roleFormPerms.value = [...role.permissions];
|
||||
roleFormDataAccess.value = clonePolicy(role.dataAccessPolicy);
|
||||
roleFormOpen.value = true;
|
||||
}
|
||||
|
||||
@@ -631,12 +637,14 @@ async function submitRoleForm() {
|
||||
description: string;
|
||||
color: RoleColor;
|
||||
permissions: RolePermissionKey[];
|
||||
dataAccessPolicy?: OrgRoleDto['dataAccessPolicy'];
|
||||
parentId?: string | null;
|
||||
} = {
|
||||
name: roleFormName.value.trim(),
|
||||
description: roleFormDesc.value.trim(),
|
||||
color: roleFormColor.value,
|
||||
permissions: roleFormPerms.value,
|
||||
dataAccessPolicy: roleFormDataAccess.value,
|
||||
};
|
||||
if (!editing?.systemKey) {
|
||||
body.parentId = roleFormParentId.value || null;
|
||||
@@ -693,6 +701,40 @@ async function toggleAccess(role: OrgRoleDto, key: RolePermissionKey) {
|
||||
}
|
||||
}
|
||||
|
||||
function clonePolicy(policy?: DataAccessPolicy | null): DataAccessPolicy {
|
||||
const src = policy ?? {};
|
||||
const out: DataAccessPolicy = {};
|
||||
for (const [key, section] of Object.entries(src)) {
|
||||
if (!section) continue;
|
||||
out[key as keyof DataAccessPolicy] = {
|
||||
mode: section.mode,
|
||||
rules: (section.rules ?? []).map((r) => ({ ...r })),
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function syncFocusDataAccess() {
|
||||
focusDataAccess.value = clonePolicy(accessFocusRole.value?.dataAccessPolicy);
|
||||
}
|
||||
|
||||
async function saveFocusDataAccess() {
|
||||
const role = accessFocusRole.value;
|
||||
if (!role || !canEditRoles.value) return;
|
||||
accessSaving.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await api.patch(`/org-roles/${role.id}`, { dataAccessPolicy: focusDataAccess.value });
|
||||
ok.value = `محدوده داده برای نقش «${role.name}» ذخیره شد.`;
|
||||
await load();
|
||||
syncFocusDataAccess();
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ذخیره محدوده داده ناموفق بود');
|
||||
} finally {
|
||||
accessSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onDocClick() {
|
||||
menuUserId.value = null;
|
||||
}
|
||||
@@ -728,10 +770,15 @@ watch(
|
||||
if (!list.some((r) => r.id === accessFocusId.value)) {
|
||||
accessFocusId.value = list[0].id;
|
||||
}
|
||||
syncFocusDataAccess();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(accessFocusId, () => {
|
||||
syncFocusDataAccess();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
syncViewport();
|
||||
window.addEventListener('resize', syncViewport);
|
||||
@@ -862,6 +909,29 @@ onUnmounted(() => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="accessFocusRole" class="data-access-block">
|
||||
<div class="matrix-head">
|
||||
<h3>محدوده داده — {{ accessFocusRole.name }}</h3>
|
||||
<button
|
||||
v-if="canEditRoles"
|
||||
type="button"
|
||||
class="btn primary"
|
||||
:disabled="accessSaving"
|
||||
@click="saveFocusDataAccess"
|
||||
>
|
||||
{{ accessSaving ? 'در حال ذخیره…' : 'ذخیره محدوده' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint">
|
||||
زیرمجموعهٔ مجوز هر بخش: اگر تنظیم نشود همه را میبیند؛ قوانین با هم «یا» میشوند.
|
||||
</p>
|
||||
<DataAccessPolicyPanel
|
||||
v-model="focusDataAccess"
|
||||
:enabled-permissions="accessFocusRole.permissions"
|
||||
:disabled="!canEditRoles"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1362,6 +1432,14 @@ onUnmounted(() => {
|
||||
{{ perm.label }}
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset class="perm-set">
|
||||
<legend>محدوده داده</legend>
|
||||
<DataAccessPolicyPanel
|
||||
v-model="roleFormDataAccess"
|
||||
:enabled-permissions="roleFormPerms"
|
||||
compact
|
||||
/>
|
||||
</fieldset>
|
||||
</form>
|
||||
<template #footer>
|
||||
<button type="button" class="btn ghost" @click="roleFormOpen = false">انصراف</button>
|
||||
@@ -1390,6 +1468,20 @@ onUnmounted(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.data-access-block {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eef2f7;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.data-access-block .hint {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
Reference in New Issue
Block a user