diff --git a/web/src/App.tsx b/web/src/App.tsx index d342f3d5..1c864c36 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -653,14 +653,21 @@ export default function App() { const [docVisibilityModalOpen, setDocVisibilityModalOpen] = useState(false); const [targetDocForVisibility, setTargetDocForVisibility] = useState(null); + const [capabilities, setCapabilities] = useState(() => authClient.getCapabilities()); + useEffect(() => { authClient.getSystemAuthConfig().then(setSystemAuthConfig).catch(() => {}); const saved = authClient.loadSavedSession(); if (saved) { setSession(saved); if (saved.env) setSystemEnv(saved.env); + setCapabilities(saved.capabilities ?? []); const match = allUsers.find((u) => u.id === saved.user.id); setCurrentUser(mergeSessionUser(saved.user, match)); + void authClient.fetchMe().then((me) => { + setCapabilities(me.capabilities ?? []); + setCurrentUser(mergeSessionUser(me.user, match)); + }).catch(() => {}); } }, []); @@ -741,19 +748,19 @@ export default function App() { // 核心功能权限计算 const canCreateProject = useMemo(() => { - return hasPermission(currentUser, roles, 'project:create'); - }, [currentUser, roles]); + return hasPermission(currentUser, roles, 'project:create', capabilities); + }, [currentUser, roles, capabilities]); const canAssignLead = useMemo(() => { - return hasPermission(currentUser, roles, 'project:assign_lead'); - }, [currentUser, roles]); + return hasPermission(currentUser, roles, 'project:assign_lead', capabilities); + }, [currentUser, roles, capabilities]); const canOverrideVisibility = useMemo(() => { return ( - hasPermission(currentUser, roles, 'doc:override_visibility') || - hasPermission(currentUser, roles, 'perm:manage') + hasPermission(currentUser, roles, 'doc:override_visibility', capabilities) || + hasPermission(currentUser, roles, 'perm:manage', capabilities) ); - }, [currentUser, roles]); + }, [currentUser, roles, capabilities]); // 项目多工作流激活状态与文档血缘 (支持前端切换与状态联动) const [projectWorkflowsState, setProjectWorkflowsState] = useState< @@ -3035,6 +3042,7 @@ export default function App() { { setSession(newSession); + setCapabilities(newSession.capabilities ?? []); if (newSession.env) setSystemEnv(newSession.env); const match = allUsers.find((u) => u.id === newSession.user.id); setCurrentUser(mergeSessionUser(newSession.user, match)); diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index f6866521..8ca50eae 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -7,6 +7,7 @@ import { RegisterResult, CreateAdminUserPayload, AccountStatus, + MeResult, } from '../types/auth'; const AUTH_STORAGE_KEY_SESSION = 'smdf_auth_session'; @@ -34,6 +35,32 @@ async function apiFetch(path: string, init?: RequestInit): Promise { export class AuthClient { constructor(_options?: AuthClientOptions) {} + async fetchMe(): Promise { + const token = this.getSessionToken(); + const resp = await apiFetch('/api/v1/auth/me', { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + if (!resp.ok) { + throw new Error('会话已过期或未登录'); + } + const data: MeResult = await resp.json(); + const saved = this.loadSavedSession(); + if (saved && data.user) { + this.saveSession({ + ...saved, + user: data.user, + env: data.env ?? saved.env, + mustChangePassword: data.must_change_password ?? saved.mustChangePassword, + capabilities: data.capabilities ?? [], + }); + } + return data; + } + + getCapabilities(): string[] { + return this.loadSavedSession()?.capabilities ?? []; + } + async login(payload: LoginPayload): Promise { const trimmedUser = payload.username.trim(); if (!trimmedUser || !payload.password) { @@ -50,6 +77,13 @@ export class AuthClient { } const data: LoginResult = await resp.json(); this.saveSession(data); + try { + const me = await this.fetchMe(); + data.capabilities = me.capabilities; + data.user = me.user; + } catch { + // login 已成功;capabilities 可在后续 refresh 补齐 + } return data; } diff --git a/web/src/engine/rbac.ts b/web/src/engine/rbac.ts index 311d1481..b79eb711 100644 --- a/web/src/engine/rbac.ts +++ b/web/src/engine/rbac.ts @@ -80,14 +80,23 @@ export const DEFAULT_ROLES: RoleDefinition[] = [ ]; /** - * 判定用户是否具备指定全局功能权限 + * 判定用户是否具备指定全局功能权限。 + * capabilities 来自服务端 /auth/me 时为权威来源;roles 仅作 UI 兜底(deprecated)。 */ export function hasPermission( user: UserProfile, roles: RoleDefinition[], - permKey: PermissionKey + permKey: PermissionKey, + capabilities?: string[] ): boolean { - // 1. 查找用户角色定义 + if (capabilities && capabilities.length > 0) { + if (capabilities.includes('perm:manage')) { + return true; + } + return capabilities.includes(permKey); + } + + // 1. 查找用户角色定义(localStorage 兜底,deprecated) const role = roles.find((r) => r.id === user.roleId); // 2. 超级管理员拥有全量特权通配 diff --git a/web/src/types/auth.ts b/web/src/types/auth.ts index 6f1cf9e6..3b3c4bce 100644 --- a/web/src/types/auth.ts +++ b/web/src/types/auth.ts @@ -26,6 +26,14 @@ export interface LoginResult { user: UserAccount; mustChangePassword?: boolean; env: string; + capabilities?: string[]; +} + +export interface MeResult { + user: UserAccount; + must_change_password?: boolean; + env: string; + capabilities: string[]; } export interface RegisterPayload {