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:
+15
-7
@@ -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));
|
||||
|
||||
@@ -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
@@ -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. 超级管理员拥有全量特权通配
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user