fix(access): honor finance sections and team-member data scope

Role treasury keys now open the first allowed finance page, and team-member ownership limits lists, dashboard, and reports instead of the whole organization.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-09 11:20:20 +03:30
co-authored by Cursor
parent 3c9b906bd6
commit 0e1bc4a1fe
15 changed files with 243 additions and 64 deletions
+12 -6
View File
@@ -21,6 +21,7 @@ import type {
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
import {
canOpenPath,
firstAllowedFinancePath,
hasSectionPermission,
sectionPermissionForPath,
} from '@/lib/section-permissions';
@@ -204,12 +205,11 @@ function cardAllowedForNav(card: {
}) {
if (card.ownerOnly && auth.user?.role !== 'owner') return false;
const perms = auth.user?.permissions ?? [];
const canTreasury = auth.user?.role === 'owner'
|| auth.user?.role === 'admin'
|| perms.includes('treasury')
|| perms.some((key) => key.startsWith('treasury_'));
if (card.to?.startsWith('/finance')) {
return moduleAllowed(card.module ?? 'treasury') && canTreasury;
return (
moduleAllowed(card.module ?? 'treasury')
&& Boolean(firstAllowedFinancePath(auth.user?.role, perms))
);
}
const section = card.to ? sectionPermissionForPath(card.to) : null;
const hasSection = section
@@ -246,7 +246,13 @@ const openHub = computed<NavHubDef | null>(() => {
if (!openHubKey.value) return null;
const hub = NAV_HUBS.find((item) => item.key === openHubKey.value);
if (!hub) return null;
const cards = hub.cards.filter((card) => cardAllowedForNav(card));
const financeTo = firstAllowedFinancePath(auth.user?.role, auth.user?.permissions);
const cards = hub.cards
.filter((card) => cardAllowedForNav(card))
.map((card) => {
if (!financeTo || !card.to.startsWith('/finance') || card.to === financeTo) return card;
return { ...card, to: financeTo };
});
if (
hub.key === 'customers' &&
isMobileNav.value &&
+24
View File
@@ -69,6 +69,30 @@ export function canOpenFinancePath(
return treasurySectionAllowed(role, permissions, section);
}
const FINANCE_ENTRIES: Array<{ path: string; section: TreasurySectionKey }> = [
{ path: '/finance', section: 'treasury_dashboard' },
{ path: '/finance/incomes', section: 'treasury_incomes' },
{ path: '/finance/expenses', section: 'treasury_expenses' },
{ path: '/finance/receivables', section: 'treasury_receivables' },
{ path: '/finance/accounts', section: 'treasury_accounts' },
{ path: '/finance/payroll', section: 'treasury_payroll' },
{ path: '/finance/recurring', section: 'treasury_recurring' },
{ path: '/finance/reports', section: 'treasury_reports' },
{ path: '/finance/audit', section: 'treasury_audit' },
{ path: '/finance/categories', section: 'treasury_categories' },
];
/** اولین صفحهٔ مالی که این نقش واقعاً می‌تواند باز کند. */
export function firstAllowedFinancePath(
role: UserRole | string | null | undefined,
permissions: readonly string[] | null | undefined,
): string | null {
for (const entry of FINANCE_ENTRIES) {
if (treasurySectionAllowed(role, permissions, entry.section)) return entry.path;
}
return null;
}
export function canOpenPath(
role: UserRole | string | null | undefined,
permissions: readonly string[] | null | undefined,
+8 -8
View File
@@ -2,7 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router';
import type { UserRole } from '@mento/shared';
import { useAuthStore } from '@/stores/auth';
import { usePlatformAuthStore } from '@/stores/platform-auth';
import { canOpenFinancePath, canOpenPath, sectionPermissionForPath } from '@/lib/section-permissions';
import {
canOpenFinancePath,
canOpenPath,
firstAllowedFinancePath,
sectionPermissionForPath,
} from '@/lib/section-permissions';
export const router = createRouter({
history: createWebHistory(),
@@ -683,13 +688,8 @@ router.beforeEach((to) => {
if (canOpenFinancePath(auth.user?.role, auth.user?.permissions, String(to.path))) {
return true;
}
const exactHome = to.path === '/finance' || to.path === '/finance/';
if (
exactHome &&
canOpenFinancePath(auth.user?.role, auth.user?.permissions, '/finance/payroll')
) {
return { path: '/finance/payroll' };
}
const next = firstAllowedFinancePath(auth.user?.role, auth.user?.permissions);
if (next && next !== to.path) return { path: next };
}
return { name: 'home' };
}
+6
View File
@@ -214,6 +214,12 @@ export const useAuthStore = defineStore('auth', () => {
if (token.value) void hydrate();
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') void hydrate();
});
}
return {
token,
user,
@@ -96,8 +96,18 @@ function docLine(item: FinancialDocumentDto) {
async function load() {
const [docs, cats, acc, bals] = await Promise.all([
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }),
api.get<FinancialAccountDto[]>('/treasury/accounts'),
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }).catch((e: unknown) => {
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
return { data: [] as FinancialCategoryDto[] };
}
throw e;
}),
api.get<FinancialAccountDto[]>('/treasury/accounts').catch((e: unknown) => {
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
return { data: [] as FinancialAccountDto[] };
}
throw e;
}),
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind } }).catch((e: unknown) => {
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };
throw e;
@@ -151,8 +151,18 @@ function docLine(item: FinancialDocumentDto) {
async function load() {
const [docs, cats, acc, dash, bals] = await Promise.all([
api.get<FinancialDocumentDto[]>('/treasury/documents', { params: { kind, q: q.value || undefined } }),
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }),
api.get<FinancialAccountDto[]>('/treasury/accounts'),
api.get<FinancialCategoryDto[]>('/treasury/categories', { params: { kind } }).catch((e: unknown) => {
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
return { data: [] as FinancialCategoryDto[] };
}
throw e;
}),
api.get<FinancialAccountDto[]>('/treasury/accounts').catch((e: unknown) => {
if (axios.isAxiosError(e) && (e.response?.status === 403 || e.response?.status === 404)) {
return { data: [] as FinancialAccountDto[] };
}
throw e;
}),
api.get<TreasuryDashboardDto>('/treasury/dashboard').catch(() => null),
api.get<TreasuryPartyBalanceDto[]>('/treasury/party-balances', { params: { kind } }).catch((e: unknown) => {
if (axios.isAxiosError(e) && e.response?.status === 404) return { data: [] as TreasuryPartyBalanceDto[] };