From 26f7d7ab3f4fb851ac8e6bcc77a43b592f567dfe Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 30 Apr 2026 17:55:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E8=83=BD=E6=BA=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=A8=A1=E5=9D=97=E9=9C=80=E8=A6=81=20BI-LEADER-ENERG?= =?UTF-8?q?Y=20=E8=A7=92=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ENERGY_ACCESS_ROLES 与 canAccessEnergy(roles) 守卫(全量权限角色亦可访问) - 后端 /api/energy/* 加模块级守卫:无角色返回 403 - 前端 App.tsx 按角色动态注入 EnergyModule,无权限时主导航不显示 - dev mock 用户(前端 + 后端)追加 BI-LEADER-ENERGY 便于本地调试 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.tsx | 15 +++++++++------ src/auth/AuthProvider.tsx | 2 +- src/server/auth/middleware.ts | 2 +- src/server/auth/types.ts | 2 ++ src/server/routes/energy/index.ts | 12 ++++++++++++ src/shared/auth/roles.ts | 9 +++++++++ 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a574898..383ca98 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,14 +10,17 @@ import FeedbackAdminPage from './modules/admin/FeedbackAdminPage'; import AuthProvider from './auth/AuthProvider'; import { useAuth } from './auth/useAuth'; import UnauthorizedPage from './auth/UnauthorizedPage'; -import { canAccessScheduling } from './shared/auth/roles'; +import { canAccessScheduling, canAccessEnergy } from './shared/auth/roles'; const BASE_MODULES: ModuleConfig[] = [ { id: 'assets', label: '资产管理', icon: Truck, component: AssetsModule }, { id: 'mileage', label: '里程管理', icon: Route, component: MileageModule }, - { id: 'energy', label: '能源管理', icon: Zap, component: EnergyModule }, ]; +const ENERGY_MODULE: ModuleConfig = { + id: 'energy', label: '能源管理', icon: Zap, component: EnergyModule, +}; + const SCHEDULING_MODULE: ModuleConfig = { id: 'scheduling', label: '智能调度', icon: Activity, component: SchedulingModule, }; @@ -47,10 +50,10 @@ function AuthGate() { }, []); const modules = useMemo(() => { - if (canAccessScheduling(user?.roles)) { - return [...BASE_MODULES, SCHEDULING_MODULE]; - } - return BASE_MODULES; + const result = [...BASE_MODULES]; + if (canAccessEnergy(user?.roles)) result.push(ENERGY_MODULE); + if (canAccessScheduling(user?.roles)) result.push(SCHEDULING_MODULE); + return result; }, [user?.roles]); if (isLoading) { diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index 4b5d398..9da7247 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -46,7 +46,7 @@ export default function AuthProvider({ children }: { children: ReactNode }) { userName: '本地开发', permissionLevel: 'full', depName: '', - roles: ['所有权限', 'BI-SCHEDULE-OPT', 'BI-ADMIN-FEEDBACK'], + roles: ['所有权限', 'BI-SCHEDULE-OPT', 'BI-ADMIN-FEEDBACK', 'BI-LEADER-ENERGY'], }, error: null, }); diff --git a/src/server/auth/middleware.ts b/src/server/auth/middleware.ts index fd2eefd..898fad9 100644 --- a/src/server/auth/middleware.ts +++ b/src/server/auth/middleware.ts @@ -23,7 +23,7 @@ export async function authMiddleware(c: Context, next: Next) { depCode: '', depName: '', permissionLevel: 'full', - roles: ['所有权限', 'BI-SCHEDULE-OPT', 'BI-ADMIN-FEEDBACK'], + roles: ['所有权限', 'BI-SCHEDULE-OPT', 'BI-ADMIN-FEEDBACK', 'BI-LEADER-ENERGY'], }; c.set('user', devUser); return next(); diff --git a/src/server/auth/types.ts b/src/server/auth/types.ts index e0970e1..20112bb 100644 --- a/src/server/auth/types.ts +++ b/src/server/auth/types.ts @@ -29,6 +29,8 @@ export { DEPT_ACCESS_ROLES, SCHEDULING_ACCESS_ROLES, FEEDBACK_ADMIN_ROLES, + ENERGY_ACCESS_ROLES, canAccessScheduling, canManageFeedback, + canAccessEnergy, } from '../../shared/auth/roles.js'; diff --git a/src/server/routes/energy/index.ts b/src/server/routes/energy/index.ts index b3d6478..10bc903 100644 --- a/src/server/routes/energy/index.ts +++ b/src/server/routes/energy/index.ts @@ -2,9 +2,21 @@ import { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; import pool from '../../db.js'; import { cached } from './cache.js'; +import type { AuthUser } from '../../auth/types.js'; +import { canAccessEnergy } from '../../auth/types.js'; const app = new Hono(); +// 模块级访问守卫:dev 旁路 auth 时 user 为 undefined,直接放行; +// 生产环境必须具备 BI-LEADER-ENERGY 或全量权限角色 +app.use('*', async (c, next) => { + const user = (c as { get: (k: string) => unknown }).get('user') as AuthUser | undefined; + if (user && !canAccessEnergy(user.roles)) { + return c.json({ error: 'Forbidden: 能源管理访问需要 BI-LEADER-ENERGY 角色' }, 403); + } + return next(); +}); + const HYDROGEN_MIN_DATE = '2024-01-01'; // hydrogen_time 已是 CST 字面值,直接使用即可(不再 +8 小时) diff --git a/src/shared/auth/roles.ts b/src/shared/auth/roles.ts index 7057cd4..2b26b7b 100644 --- a/src/shared/auth/roles.ts +++ b/src/shared/auth/roles.ts @@ -13,6 +13,9 @@ export const SCHEDULING_ACCESS_ROLES = ['BI-SCHEDULE-OPT']; /** 反馈管理(管理员)访问角色 */ export const FEEDBACK_ADMIN_ROLES = ['BI-ADMIN-FEEDBACK']; +/** 能源管理模块访问角色 */ +export const ENERGY_ACCESS_ROLES = ['BI-LEADER-ENERGY']; + /** 用户是否可访问智能调度模块。仅 BI-SCHEDULE-OPT 角色允许访问。 */ export function canAccessScheduling(roles: readonly string[] | null | undefined): boolean { if (!roles || roles.length === 0) return false; @@ -24,3 +27,9 @@ export function canManageFeedback(roles: readonly string[] | null | undefined): if (!roles || roles.length === 0) return false; return roles.some(r => FEEDBACK_ADMIN_ROLES.includes(r) || FULL_ACCESS_ROLES.includes(r)); } + +/** 用户是否可访问能源管理模块。BI-LEADER-ENERGY 或全量权限角色可访问。 */ +export function canAccessEnergy(roles: readonly string[] | null | undefined): boolean { + if (!roles || roles.length === 0) return false; + return roles.some(r => ENERGY_ACCESS_ROLES.includes(r) || FULL_ACCESS_ROLES.includes(r)); +}