feat: add permission-based access control and improve alert table UI

- Implement PermissionGuard component for route-level permission enforcement
- Add tree-based permission checkbox group in role management popup
- Gate SSE connection, unread count fetch, and rule edit actions on user permissions
- Add skip401Refresh option and auto token refresh on 401 response
- Improve alert table with fixed columns, explicit widths, and text ellipsis
- Add i18n entries for permission guide and rule menu descriptions (en/zh)
- Add expo-api nginx proxy configuration
- Add .claude to .gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 18:04:07 +08:00
parent 710da86521
commit 43d6e9207a
20 changed files with 742 additions and 48 deletions

View File

@@ -0,0 +1,48 @@
import { history, useAccess } from '@umijs/max';
import { useCallback, useEffect, useRef, useState } from 'react';
import useUserInfo from '@/hooks/useUserInfo';
import { currentUser as queryCurrentUser } from '@/services/api';
import { getRequiredPermissions } from '@/utils/routePermissions';
export default function PermissionGuard() {
const { setUserInfo } = useUserInfo();
const access = useAccess();
const [checkVersion, setCheckVersion] = useState(0);
const pathnameRef = useRef('');
const refreshing = useRef(false);
const onAuthRefresh = useCallback(async () => {
if (refreshing.current) return;
refreshing.current = true;
pathnameRef.current = history.location.pathname;
const res = (await queryCurrentUser()) as API.BaseResponse<API.CurrentUser>;
if (res?.code === 200 && res?.data?.data) {
await setUserInfo(res.data.data);
setCheckVersion((v) => v + 1);
}
refreshing.current = false;
}, [setUserInfo]);
useEffect(() => {
if (checkVersion === 0) return;
const required = getRequiredPermissions(pathnameRef.current);
if (required?.length) {
const hasAccess = required.some(
(perm) => access?.hasPerms?.(perm) ?? false,
);
if (!hasAccess) {
history.replace('/404', { from: pathnameRef.current });
}
}
}, [checkVersion, access]);
useEffect(() => {
window.addEventListener('auth:refresh', onAuthRefresh);
return () => window.removeEventListener('auth:refresh', onAuthRefresh);
}, [onAuthRefresh]);
return null;
}

View File

@@ -1,4 +1,4 @@
import { useModel } from '@umijs/max';
import { useAccess, useModel } from '@umijs/max';
import React, { useEffect, useRef } from 'react';
import { useMessageBuffer } from '@/hooks/useMessage';
import useUserInfo from '@/hooks/useUserInfo';
@@ -11,11 +11,13 @@ const SSEController: React.FC = () => {
const { disconnectSSE } = useModel('useSSE');
const { startConnection } = useMessageBuffer();
const { userInfo } = useUserInfo();
const { hasPerms } = useAccess();
const userConfig = userInfo?.userConfig;
const timerRef = useRef<NodeJS.Timeout | null>(null);
const { setUnreadCountWithSign } = useModel('useMenuNoticeNum');
const refreshCount = async () => {
if (!hasPerms('alert:list')) return;
try {
const { data } = await getAlertPages({
pageNum: 1,
@@ -30,6 +32,7 @@ const SSEController: React.FC = () => {
};
const startPolling = () => {
stopPolling();
if (!hasPerms('alert:list')) return;
timerRef.current = setInterval(() => {
refreshCount();
}, TIMER_INTERVAL);