feat(frontend): enforce RBAC and refine burst analysis

This commit is contained in:
2026-07-30 16:45:10 +08:00
parent f355ddd002
commit 723931b6ae
15 changed files with 772 additions and 390 deletions
+20 -43
View File
@@ -49,6 +49,8 @@ import {
} from "@mui/icons-material";
import { config } from "@config/config";
import { apiFetch } from "@/lib/apiFetch";
import { permissionCodes } from "@/lib/permissions";
import { useAccessStore } from "@/store/accessStore";
type AuditLog = {
id: string;
@@ -105,6 +107,8 @@ const defaultFilters: AuditFilters = {
end_time: "",
};
const AUDIT_LOGS_PATH = "/api/v1/audit/logs";
const statusFilterOptions: Array<{ value: AuditStatusFilter; label: string }> = [
{ value: "all", label: "全部状态" },
{ value: "success", label: "成功 2xx" },
@@ -338,8 +342,14 @@ const DetailSection = ({
);
export const AuditLogPanel = () => {
const [adminChecked, setAdminChecked] = useState(false);
const [isAuthorized, setIsAuthorized] = useState(false);
const accessLoading = useAccessStore((state) => state.loading);
const permissions = useAccessStore((state) => state.permissions);
const isSystemAdmin = useAccessStore(
(state) => state.context?.is_system_admin === true,
);
const canViewAudit = permissions.includes(permissionCodes.auditView);
const adminChecked = !accessLoading;
const isAuthorized = adminChecked && canViewAudit && isSystemAdmin;
const [logs, setLogs] = useState<AuditLog[]>([]);
const [users, setUsers] = useState<MetadataUser[]>([]);
const [projects, setProjects] = useState<AdminProject[]>([]);
@@ -392,8 +402,8 @@ export const AuditLogPanel = () => {
const params = buildServerParams(appliedFilters, page * rowsPerPage, rowsPerPage);
const countParams = buildCountParams(appliedFilters);
const [logsResponse, countResponse] = await Promise.all([
apiFetch(`${config.BACKEND_URL}/api/v1/audit/logs?${params.toString()}`),
apiFetch(`${config.BACKEND_URL}/api/v1/audit/logs/count?${countParams.toString()}`),
apiFetch(`${config.BACKEND_URL}${AUDIT_LOGS_PATH}?${params.toString()}`),
apiFetch(`${config.BACKEND_URL}${AUDIT_LOGS_PATH}/count?${countParams.toString()}`),
]);
if (!logsResponse.ok) throw new Error(await readErrorText(logsResponse));
if (!countResponse.ok) throw new Error(await readErrorText(countResponse));
@@ -402,7 +412,7 @@ export const AuditLogPanel = () => {
setTotalCount(Number(countPayload.count ?? 0));
} else {
const params = buildServerParams(appliedFilters, 0, 1000);
const response = await apiFetch(`${config.BACKEND_URL}/api/v1/audit/logs?${params.toString()}`);
const response = await apiFetch(`${config.BACKEND_URL}${AUDIT_LOGS_PATH}?${params.toString()}`);
if (!response.ok) throw new Error(await readErrorText(response));
const payload = (await response.json()) as AuditLog[];
setLogs(payload);
@@ -417,42 +427,9 @@ export const AuditLogPanel = () => {
}, [appliedFilters, page, rowsPerPage]);
useEffect(() => {
let cancelled = false;
const checkAdmin = async () => {
try {
const response = await apiFetch(`${config.BACKEND_URL}/api/v1/admin/me`, {
projectHeaderMode: "omit",
skipAuthRedirect: true,
});
if (cancelled) return;
if (!response.ok) {
setIsAuthorized(false);
setAdminChecked(true);
return;
}
const payload = await response.json();
setIsAuthorized(Boolean(payload?.is_superuser || payload?.role === "admin"));
setAdminChecked(true);
} catch (err) {
if (!cancelled) {
setIsAuthorized(false);
setAdminChecked(true);
setError(String(err));
}
}
};
void checkAdmin();
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (!adminChecked || !isAuthorized) return;
if (!isAuthorized) return;
void loadOptions();
}, [adminChecked, isAuthorized, loadOptions]);
}, [isAuthorized, loadOptions]);
useEffect(() => {
if (!adminChecked || !isAuthorized) return;
@@ -476,7 +453,7 @@ export const AuditLogPanel = () => {
setError(null);
try {
const params = buildServerParams(appliedFilters, 0, 1000);
const response = await apiFetch(`${config.BACKEND_URL}/api/v1/audit/logs?${params.toString()}`);
const response = await apiFetch(`${config.BACKEND_URL}${AUDIT_LOGS_PATH}?${params.toString()}`);
if (!response.ok) throw new Error(await readErrorText(response));
const payload = ((await response.json()) as AuditLog[]).filter((log) =>
matchesStatusFilter(log, appliedFilters.status),
@@ -544,7 +521,7 @@ export const AuditLogPanel = () => {
</Typography>
<Typography variant="body2" color="text.secondary">
</Typography>
</Box>
</Stack>
@@ -552,7 +529,7 @@ export const AuditLogPanel = () => {
<Chip
color="success"
icon={<AdminPanelSettingsIcon />}
label="管理员权限已验证"
label="系统管理员权限已验证"
sx={{ alignSelf: { xs: "flex-start", md: "center" } }}
/>
)}