feat(frontend): enforce RBAC and refine burst analysis
This commit is contained in:
@@ -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" } }}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user