feat(web): consume server capabilities for RBAC decisions

Fetch and cache capabilities from /auth/me on login and session restore; hasPermission prefers server capabilities over local role definitions.
This commit is contained in:
weichen
2026-09-13 02:50:22 +00:00
committed by weichen
parent 4258d88415
commit 844bfd2803
4 changed files with 69 additions and 10 deletions
+15 -7
View File
@@ -653,14 +653,21 @@ export default function App() {
const [docVisibilityModalOpen, setDocVisibilityModalOpen] = useState(false);
const [targetDocForVisibility, setTargetDocForVisibility] = useState<DeliverableItem | null>(null);
const [capabilities, setCapabilities] = useState<string[]>(() => 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() {
<LoginView
onLoginSuccess={(newSession) => {
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));
+34
View File
@@ -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<Response> {
export class AuthClient {
constructor(_options?: AuthClientOptions) {}
async fetchMe(): Promise<MeResult> {
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<LoginResult> {
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;
}
+12 -3
View File
@@ -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. 超级管理员拥有全量特权通配
+8
View File
@@ -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 {