From a0dc6fc408eaa24a325166e74d55e69eb7104b4e Mon Sep 17 00:00:00 2001 From: shunli Date: Tue, 12 May 2026 08:29:18 +0800 Subject: [PATCH] =?UTF-8?q?PA6=20=E8=A1=A5=E9=BD=90=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=B8=8E=E5=AE=A1=E8=AE=A1=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/auth/permissions.ts | 24 + .../src/components/mobile/MobileBottomNav.tsx | 17 +- client/src/components/navigation/NavLinks.tsx | 13 +- client/src/pages/MobileMePage.tsx | 13 +- client/src/pages/MobilePortalPage.tsx | 25 +- client/src/router/guards.tsx | 16 +- client/src/router/index.tsx | 18 +- .../e2e/permission-direct-access.spec.ts | 476 ++++++++++++++++++ ...1-46-00-r6-permission-audit-regression.csv | 10 +- scripts/qa/run-business-flow-gates.sh | 1 + server/src/routes/reimbursements.js | 12 + server/src/services/workspaceSettings.js | 36 +- .../unit/workspaceSettings.navigation.test.js | 26 + 13 files changed, 645 insertions(+), 42 deletions(-) diff --git a/client/src/auth/permissions.ts b/client/src/auth/permissions.ts index be372304..bf12c12a 100644 --- a/client/src/auth/permissions.ts +++ b/client/src/auth/permissions.ts @@ -155,6 +155,17 @@ const ROLE_PERMISSIONS: Record = { admin: ["*"], }; +export const BUSINESS_APPROVAL_PERMISSIONS = [ + "expense.approve", + "leave.approve", + "procurement.approve", + "travel.approve", +] as const; + +const BUSINESS_APPROVER_ROLE_CODES = ["manager", "ceo", "finance", "finance_mgr", "hr"] as const; +const FINANCE_ROLE_CODES = ["finance", "finance_mgr"] as const; +const FINANCE_PORTAL_PERMISSIONS = ["payment.batch.manage", "risk.review.manage", "expense.finance.pay"] as const; + function rolePermissions(roleCode: string): readonly string[] { return ROLE_PERMISSIONS[roleCode] ?? []; } @@ -183,6 +194,19 @@ export function hasAnyPermission(permissionCodes: string[], candidates: string[] return candidates.some((candidate) => hasPermission(permissionCodes, candidate)); } +export function hasBusinessApproverRole(roleCodes: string[] | undefined, permissionCodes: string[]) { + const hasBusinessRole = (roleCodes ?? []).some((roleCode) => + (BUSINESS_APPROVER_ROLE_CODES as readonly string[]).includes(roleCode), + ); + + return hasBusinessRole && hasAnyPermission(permissionCodes, [...BUSINESS_APPROVAL_PERMISSIONS]); +} + +export function hasFinanceRole(roleCodes: string[] | undefined, permissionCodes: string[]) { + const hasRole = (roleCodes ?? []).some((roleCode) => (FINANCE_ROLE_CODES as readonly string[]).includes(roleCode)); + return hasRole && hasAnyPermission(permissionCodes, [...FINANCE_PORTAL_PERMISSIONS]); +} + export function isManagerRole(permissionCodes: string[]) { if (hasPermission(permissionCodes, "*") || hasPermission(permissionCodes, "system.users")) { return true; diff --git a/client/src/components/mobile/MobileBottomNav.tsx b/client/src/components/mobile/MobileBottomNav.tsx index 19ab0723..27b94b2d 100644 --- a/client/src/components/mobile/MobileBottomNav.tsx +++ b/client/src/components/mobile/MobileBottomNav.tsx @@ -2,11 +2,8 @@ import { NavLink } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { fetchWorkflowTasks } from "../../api/workflow"; import { useAuth } from "../../auth/AuthContext"; -import { hasAnyPermission } from "../../auth/permissions"; -import { - MOBILE_APPROVAL_PERMISSIONS, - MOBILE_NAV_ITEMS, -} from "../../features/mobile/navigation"; +import { hasAnyPermission, hasBusinessApproverRole } from "../../auth/permissions"; +import { MOBILE_NAV_ITEMS } from "../../features/mobile/navigation"; import { cn } from "../../lib/utils"; import { MobileIcon } from "./MobileIcon"; @@ -18,8 +15,8 @@ function resolveBadgeLabel(count: number) { } export function MobileBottomNav() { - const { permissions } = useAuth(); - const canAccessApprovals = hasAnyPermission(permissions, [...MOBILE_APPROVAL_PERMISSIONS]); + const { me, permissions } = useAuth(); + const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions); const pendingCountQuery = useQuery({ enabled: canAccessApprovals, @@ -40,7 +37,11 @@ export function MobileBottomNav() { >
{MOBILE_NAV_ITEMS.map((item) => { - const disabled = item.anyOf ? !hasAnyPermission(permissions, [...item.anyOf]) : false; + const disabled = item.key === "approvals" + ? !canAccessApprovals + : item.anyOf + ? !hasAnyPermission(permissions, [...item.anyOf]) + : false; const badge = item.key === "approvals" && pendingCount > 0 ? resolveBadgeLabel(pendingCount) : null; if (disabled) { diff --git a/client/src/components/navigation/NavLinks.tsx b/client/src/components/navigation/NavLinks.tsx index b8b59a41..5e5272ba 100644 --- a/client/src/components/navigation/NavLinks.tsx +++ b/client/src/components/navigation/NavLinks.tsx @@ -2,7 +2,7 @@ import { NavLink } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { fetchSystemNavigation, type NavigationItem } from "../../api/workspace"; import { useAuth } from "../../auth/AuthContext"; -import { hasAnyPermission } from "../../auth/permissions"; +import { hasAnyPermission, hasBusinessApproverRole, hasFinanceRole } from "../../auth/permissions"; import { ADMIN_NAV_ORDER, BACKUP_NAV_ORDER, @@ -32,7 +32,7 @@ export const NAV_ITEMS: NavItem[] = [ { to: "/people", label: "人员管理", icon: "group", anyOf: ["system.users", "report.dashboard.view"] }, { to: "/organization", label: "组织架构", icon: "account_tree", anyOf: ["system.users", "report.dashboard.view"] }, { to: "/system/workflow-definitions", label: "流程定义", icon: "schema", anyOf: ["system.settings"] }, - { to: "/system/settings", label: "系统设置", icon: "settings", anyOf: ["system.settings", "system.users", "masterdata.view", "masterdata.manage", "notification.channel.manage", "integration.manage"] }, + { to: "/system/settings", label: "系统设置", icon: "settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage"] }, { to: "/system/feature-flags", label: "功能开关", icon: "toggle_on", anyOf: ["system.settings"] }, { to: "/audit-logs", label: "审计日志", icon: "manage_search", anyOf: ["system.audit.view"] }, { to: "/workflow/tasks", label: "审批待办", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"] }, @@ -136,9 +136,12 @@ function renderBackupSection(items: ResolvedNavItem[], onNavigate?: () => void) ); } -function buildFallbackItems(permissions: string[]) { +function buildFallbackItems(permissions: string[], roleCodes: string[]) { + const financeOnlyPaths = new Set(["/payment-batches", "/ap-operations", "/invoice-pool"]); const mapItems = (paths: string[]) => NAV_ITEMS.filter((item) => paths.includes(item.to)) + .filter((item) => item.to !== "/workflow/tasks" || hasBusinessApproverRole(roleCodes, permissions)) + .filter((item) => !financeOnlyPaths.has(item.to) || hasFinanceRole(roleCodes, permissions)) .filter((item) => !item.anyOf || hasAnyPermission(permissions, item.anyOf)) .map((item) => ({ key: item.to, href: item.to, label: item.label, icon: item.icon })); @@ -184,7 +187,7 @@ function normalizeSections(sections: { main: NavigationItem[]; admin: Navigation } export function NavLinks({ onNavigate }: { onNavigate?: () => void }) { - const { permissions } = useAuth(); + const { me, permissions } = useAuth(); const navigationQuery = useQuery({ queryKey: ["system-navigation"], queryFn: fetchSystemNavigation, @@ -193,7 +196,7 @@ export function NavLinks({ onNavigate }: { onNavigate?: () => void }) { const sections = navigationQuery.data?.sections ? normalizeSections(navigationQuery.data.sections) - : buildFallbackItems(permissions); + : buildFallbackItems(permissions, me?.roleCodes ?? []); return (