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:
48
src/components/PermissionGuard.tsx
Normal file
48
src/components/PermissionGuard.tsx
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user