feat: enforce role section permissions and add a leads permission
Unticking a section hides it from the menu, closes its routes and blocks its writes on the server; existing roles keep their current access via migration.
This commit is contained in:
@@ -19,6 +19,11 @@ import type {
|
||||
NotificationsListDto,
|
||||
} from '@mento/shared';
|
||||
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
|
||||
import {
|
||||
canOpenPath,
|
||||
hasSectionPermission,
|
||||
sectionPermissionForPath,
|
||||
} from '@/lib/section-permissions';
|
||||
import { refreshUnreadTotal, useOrgChat } from '@/lib/useOrgChat';
|
||||
|
||||
const auth = useAuthStore();
|
||||
@@ -145,6 +150,11 @@ const moreSectionActive = computed(
|
||||
const enabledModules = computed(() => auth.user?.enabledModules ?? null);
|
||||
const subscription = computed(() => auth.user?.subscription ?? null);
|
||||
|
||||
/** لینکهای مستقیم منو (بیرون از هابها) با مجوز بخش */
|
||||
function canOpen(path: string) {
|
||||
return canOpenPath(auth.user?.role, auth.user?.permissions, path);
|
||||
}
|
||||
|
||||
function moduleAllowed(module?: string) {
|
||||
if (!module) return true;
|
||||
if (!enabledModules.value) return true;
|
||||
@@ -163,7 +173,16 @@ function cardAllowedForNav(card: {
|
||||
|| Boolean(auth.user?.permissions?.includes('treasury'))
|
||||
|| Boolean(auth.user?.permissions?.includes('treasury_payroll'));
|
||||
if (card.to?.startsWith('/finance')) return canTreasury;
|
||||
if (card.admin && !canSettings.value && !canOpenTeamWhileViewing.value) return false;
|
||||
const section = card.to ? sectionPermissionForPath(card.to) : null;
|
||||
const hasSection = section
|
||||
? hasSectionPermission(auth.user?.role, auth.user?.permissions, section)
|
||||
: true;
|
||||
if (!hasSection) return false;
|
||||
// لاگ فعالیت با تیک «لاگ فعالیت کاربران» برای غیرمدیر هم باز میشود
|
||||
const grantedBySection = section === 'audit' && hasSection;
|
||||
if (card.admin && !canSettings.value && !canOpenTeamWhileViewing.value && !grantedBySection) {
|
||||
return false;
|
||||
}
|
||||
if (!moduleAllowed(card.module)) return false;
|
||||
if (
|
||||
card.admin &&
|
||||
@@ -607,6 +626,7 @@ onUnmounted(() => {
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink
|
||||
v-if="canOpen('/notes')"
|
||||
to="/notes"
|
||||
class="nav-item"
|
||||
title="یادداشتها"
|
||||
@@ -636,7 +656,13 @@ onUnmounted(() => {
|
||||
<UiIcon name="gift" :size="collapsed ? 20 : 18" />
|
||||
<span>باشگاه مشتریان</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/reports" class="nav-item" title="گزارشها" @click="closeHub(); closeMenu()">
|
||||
<RouterLink
|
||||
v-if="canOpen('/reports')"
|
||||
to="/reports"
|
||||
class="nav-item"
|
||||
title="گزارشها"
|
||||
@click="closeHub(); closeMenu()"
|
||||
>
|
||||
<UiIcon name="chart" :size="collapsed ? 20 : 18" />
|
||||
<span>گزارشها</span>
|
||||
</RouterLink>
|
||||
@@ -807,14 +833,24 @@ onUnmounted(() => {
|
||||
</form>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<RouterLink class="round-btn accent" :to="{ name: 'notes' }" title="هوش مصنوعی">
|
||||
<RouterLink
|
||||
v-if="canOpen('/notes')"
|
||||
class="round-btn accent"
|
||||
:to="{ name: 'notes' }"
|
||||
title="هوش مصنوعی"
|
||||
>
|
||||
<UiIcon name="sparkle" :size="18" />
|
||||
</RouterLink>
|
||||
<button class="round-btn" type="button" aria-label="اعلانها" @click="toggleNotif">
|
||||
<UiIcon name="bell" :size="18" />
|
||||
<span v-if="hasUnread" class="badge">{{ toFaDigits(unreadCount) }}</span>
|
||||
</button>
|
||||
<RouterLink class="round-btn" :to="{ name: 'activities-calendar' }" title="تقویم">
|
||||
<RouterLink
|
||||
v-if="canOpen('/activities')"
|
||||
class="round-btn"
|
||||
:to="{ name: 'activities-calendar' }"
|
||||
title="تقویم"
|
||||
>
|
||||
<UiIcon name="calendar" :size="18" />
|
||||
</RouterLink>
|
||||
<RouterLink class="round-btn" :to="{ name: 'tickets' }" title="پیامها">
|
||||
@@ -897,6 +933,7 @@ onUnmounted(() => {
|
||||
<span>داشبورد</span>
|
||||
</RouterLink>
|
||||
<button
|
||||
v-if="hubVisible.customers || hubVisible.leads"
|
||||
class="tab"
|
||||
type="button"
|
||||
:class="{
|
||||
@@ -915,6 +952,7 @@ onUnmounted(() => {
|
||||
<span>مشتریان</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="hubVisible.sales"
|
||||
class="tab"
|
||||
type="button"
|
||||
:class="{ on: !moreOpen && (salesActive || openHubKey === 'sales') }"
|
||||
@@ -925,6 +963,7 @@ onUnmounted(() => {
|
||||
<span>فروش</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="hubVisible.activities"
|
||||
class="tab"
|
||||
type="button"
|
||||
:class="{ on: !moreOpen && (activitiesActive || openHubKey === 'activities') }"
|
||||
@@ -984,7 +1023,7 @@ onUnmounted(() => {
|
||||
<UiIcon name="trophy" :size="18" />
|
||||
<span>باشگاه امتیازات</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/notes" class="nav-item" @click="closeHub(); closeMore()">
|
||||
<RouterLink v-if="canOpen('/notes')" to="/notes" class="nav-item" @click="closeHub(); closeMore()">
|
||||
<UiIcon name="document" :size="18" />
|
||||
<span>یادداشتها</span>
|
||||
</RouterLink>
|
||||
@@ -1017,7 +1056,7 @@ onUnmounted(() => {
|
||||
<UiIcon name="gift" :size="18" />
|
||||
<span>باشگاه مشتریان</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/reports" class="nav-item" @click="closeHub(); closeMore()">
|
||||
<RouterLink v-if="canOpen('/reports')" to="/reports" class="nav-item" @click="closeHub(); closeMore()">
|
||||
<UiIcon name="chart" :size="18" />
|
||||
<span>گزارشها</span>
|
||||
</RouterLink>
|
||||
|
||||
Reference in New Issue
Block a user