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:
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
|
||||
@@ -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[] };
|
||||
|
||||
Reference in New Issue
Block a user