Files
CollegeAIcenter/web/src/app/admin/compliance/page.tsx
T

188 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/** 权限与合规:查看三类角色权限范围、审计日志与越权拒绝事件(可按操作者过滤)。 */
import { useCallback, useEffect, useState } from "react";
import { ShieldCheck } from "lucide-react";
import {
EmptyState,
ErrorBanner,
Loading,
PageHeading,
} from "@/components/feedback";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { UsageGuide } from "@/components/usage-guide";
import { ApiError } from "@/lib/api";
import { adminComplianceApi } from "@/lib/services";
/* eslint-disable @typescript-eslint/no-explicit-any */
export default function AdminCompliancePage() {
const [scopes, setScopes] = useState<any[]>([]);
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [denials, setDenials] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [actorId, setActorId] = useState("");
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [sc, logs, dn] = await Promise.all([
adminComplianceApi.permissionScopes(),
adminComplianceApi.auditLogs(actorId || undefined),
adminComplianceApi.denialEvents(actorId || undefined),
]);
setScopes(Array.isArray(sc) ? sc : []);
setAuditLogs(Array.isArray(logs) ? logs : []);
setDenials(Array.isArray(dn) ? dn : []);
} catch (err) {
setError(err instanceof ApiError ? err.message : "加载失败");
} finally {
setLoading(false);
}
}, [actorId]);
useEffect(() => {
load();
}, [load]);
return (
<div className="space-y-6">
<PageHeading
icon={<ShieldCheck className="size-5" />}
title="权限与合规"
description="查看角色权限范围、审计日志与越权拒绝事件。"
/>
<UsageGuide
steps={[
{ title: "查看角色权限", detail: "顶部按学生 / 导师 / 管理员列出各自的权限范围。" },
{ title: "按操作者过滤", detail: "在输入框填写操作者标识,点击「查询」缩小范围。" },
{ title: "审阅审计日志", detail: "左侧查看操作审计记录,追溯关键行为。" },
{ title: "排查越权事件", detail: "右侧查看被拒绝的越权访问,及时发现异常。" },
]}
tip="越权拒绝事件是合规风险信号,建议定期结合操作者标识排查。"
/>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<Loading />
) : error ? (
<ErrorBanner message={error} />
) : (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{scopes.map((sc: any, i: number) => (
<div
key={sc.role ?? i}
className="rounded-lg border border-border p-3"
>
<p className="mb-2 text-sm font-semibold text-primary">
{sc.role}
</p>
<ul className="space-y-1 text-xs text-muted-foreground">
{(sc.permissions ?? []).map((p: any, pi: number) => (
<li key={pi}>
{typeof p === "string" ? (
<>· {p}</>
) : (
<>
·{" "}
<span className="font-medium">{p.resourceType}</span>
{Array.isArray(p.actions) ? p.actions.join(" / ") : ""}
</>
)}
</li>
))}
</ul>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardAction>
<div className="flex items-center gap-2">
<Input
className="w-48"
value={actorId}
onChange={(e) => setActorId(e.target.value)}
placeholder="按操作者标识过滤"
/>
<Button variant="outline" size="sm" onClick={load} disabled={loading}>
</Button>
</div>
</CardAction>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div>
<h3 className="mb-2 text-sm font-semibold text-foreground">
{auditLogs.length}
</h3>
{auditLogs.length === 0 ? (
<EmptyState message="暂无审计日志。" />
) : (
<ul className="space-y-2">
{auditLogs.map((log: any, i: number) => (
<li
key={log.id ?? i}
className="rounded-lg border border-border p-3 text-xs text-muted-foreground"
>
<pre className="scrollbar-thin overflow-auto">
{JSON.stringify(log, null, 2)}
</pre>
</li>
))}
</ul>
)}
</div>
<div>
<h3 className="mb-2 text-sm font-semibold text-foreground">
{denials.length}
</h3>
{denials.length === 0 ? (
<EmptyState message="暂无越权拒绝事件。" />
) : (
<ul className="space-y-2">
{denials.map((d: any, i: number) => (
<li
key={d.id ?? i}
className="rounded-lg border border-destructive/20 bg-destructive/10 p-3 text-xs text-destructive"
>
<pre className="scrollbar-thin overflow-auto">
{JSON.stringify(d, null, 2)}
</pre>
</li>
))}
</ul>
)}
</div>
</div>
</CardContent>
</Card>
</div>
);
}