PA6 补齐权限与审计回归

这个提交包含在:
2026-05-12 08:29:18 +08:00
父节点 aec660a8fe
当前提交 a0dc6fc408
共修改 13 个文件,包含 645 行新增和 42 行删除
+24
查看文件
@@ -155,6 +155,17 @@ const ROLE_PERMISSIONS: Record<string, readonly string[]> = {
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;
@@ -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() {
>
<div className="relative z-[71] grid grid-cols-4 gap-1">
{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) {
@@ -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 (
<nav className="space-y-5">
+10 -3
查看文件
@@ -6,7 +6,7 @@ import { fetchEmployeeProfile } from "../api/employees";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { hasAnyPermission, hasBusinessApproverRole } from "../auth/permissions";
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
import { PageHeader } from "../components/common/PageHeader";
import { MobileIcon } from "../components/mobile/MobileIcon";
@@ -52,8 +52,15 @@ function MobileActionLink({
export function MobileMePage() {
const { me, permissions, refreshMe, logout } = useAuth();
const canAccessApprovals = hasAnyPermission(permissions, ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"]);
const canAccessAdmin = hasAnyPermission(permissions, ["*", "system.settings", "system.users", "report.dashboard.view"]);
const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions);
const canAccessAdmin = hasAnyPermission(permissions, [
"*",
"system.settings",
"system.users",
"masterdata.manage",
"notification.channel.manage",
"integration.manage",
]);
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
const canAccessProjects = hasAnyPermission(permissions, ["project.view_own", "project.view_all", "project.manage"]);
+13 -12
查看文件
@@ -12,13 +12,12 @@ import { fetchNotifications } from "../api/notifications";
import { isApiError } from "../api/types";
import { fetchWorkflowTasks } from "../api/workflow";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { hasAnyPermission, hasBusinessApproverRole } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { MobileIcon } from "../components/mobile/MobileIcon";
import { Button } from "../components/ui/button";
import {
MOBILE_APPROVAL_PERMISSIONS,
MOBILE_CAPTURE_PERMISSIONS,
MOBILE_LEAVE_PERMISSIONS,
MOBILE_REIMBURSEMENT_PERMISSIONS,
@@ -155,8 +154,8 @@ function ActionTile({ action }: { action: MobileAction }) {
export function MobilePortalPage() {
const queryClient = useQueryClient();
const { permissions } = useAuth();
const canAccessApprovals = hasAnyPermission(permissions, [...MOBILE_APPROVAL_PERMISSIONS]);
const { me, permissions } = useAuth();
const canAccessApprovals = hasBusinessApproverRole(me?.roleCodes, permissions);
const canAccessCapture = hasAnyPermission(permissions, [...MOBILE_CAPTURE_PERMISSIONS]);
const canAccessLeave = hasAnyPermission(permissions, [...MOBILE_LEAVE_PERMISSIONS]);
const canAccessReimbursements = hasAnyPermission(permissions, [...MOBILE_REIMBURSEMENT_PERMISSIONS]);
@@ -217,14 +216,16 @@ export function MobilePortalPage() {
const unreadNotificationCount = notifications.filter((item) => !item.readAt).length;
const actions: MobileAction[] = [
{
key: "approvals",
label: "审批",
meta: pendingApprovalCount > 0 ? `${pendingApprovalCount} 条待处理` : "暂无待办",
icon: "fact_check",
to: "/mobile/approvals",
tone: pendingApprovalCount > 0 ? "warning" : "primary",
},
canAccessApprovals
? {
key: "approvals",
label: "审批",
meta: pendingApprovalCount > 0 ? `${pendingApprovalCount} 条待处理` : "暂无待办",
icon: "fact_check",
to: "/mobile/approvals",
tone: pendingApprovalCount > 0 ? "warning" : "primary",
}
: null,
canAccessCapture
? {
key: "capture",
+15 -1
查看文件
@@ -1,7 +1,7 @@
import type { PropsWithChildren } from "react";
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { hasAnyPermission, hasBusinessApproverRole } from "../auth/permissions";
import { resolveDefaultWorkspacePath } from "../lib/runtimeContext";
import { isOfflineSnapshotAllowedPath } from "../pwa";
@@ -78,3 +78,17 @@ export function RequirePermission({
return <>{children}</>;
}
export function RequireBusinessApprover({ children }: PropsWithChildren) {
const { me, permissions, ready } = useAuth();
if (!ready) {
return <FullPageLoading />;
}
if (!hasBusinessApproverRole(me?.roleCodes, permissions)) {
return <Navigate to="/403" replace />;
}
return <>{children}</>;
}
+11 -7
查看文件
@@ -68,7 +68,7 @@ import {
WorkflowTaskDetailPage,
WorkflowTasksPage,
} from "./lazy";
import { GuestOnly, RequireAuth, RequirePermission } from "./guards";
import { GuestOnly, RequireAuth, RequireBusinessApprover, RequirePermission } from "./guards";
function withRouteFallback(node: ReactNode) {
return <Suspense fallback={<RoutePageFallback />}>{node}</Suspense>;
@@ -277,18 +277,18 @@ export function AppRouter() {
<Route
path="workflow/tasks"
element={
<RequirePermission anyOf={["expense.approve", "leave.approve", "procurement.approve", "travel.approve"]}>
<RequireBusinessApprover>
{withRouteFallback(<WorkflowTasksPage />)}
</RequirePermission>
</RequireBusinessApprover>
}
/>
<Route
path="workflow/tasks/:taskId"
element={
<RequirePermission anyOf={["expense.approve", "leave.approve", "procurement.approve", "travel.approve"]}>
<RequireBusinessApprover>
{withRouteFallback(<WorkflowTaskDetailPage />)}
</RequirePermission>
</RequireBusinessApprover>
}
/>
@@ -358,7 +358,11 @@ export function AppRouter() {
<Route
path="mobile/approvals"
element={withRouteFallback(<MobileApprovalsPage />)}
element={
<RequireBusinessApprover>
{withRouteFallback(<MobileApprovalsPage />)}
</RequireBusinessApprover>
}
/>
<Route path="notifications" element={withRouteFallback(<NotificationsPage />)} />
@@ -485,7 +489,7 @@ export function AppRouter() {
<Route
path="system/settings"
element={
<RequirePermission anyOf={["system.settings", "system.users", "masterdata.view", "masterdata.manage", "notification.channel.manage", "integration.manage"]}>
<RequirePermission anyOf={["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage"]}>
{withRouteFallback(<SystemSettingsPage />)}
</RequirePermission>
}