feat(admin): add operations resources and audit UI

This commit is contained in:
Jesse_Chen
2026-08-06 20:15:08 +08:00
parent 28e04857fa
commit 5622ab448a
39 changed files with 3770 additions and 293 deletions
@@ -0,0 +1,2 @@
import AdministratorsResource from "@/components/admin/administrators-resource";
export default function Page() { return <AdministratorsResource />; }
@@ -0,0 +1,2 @@
import AuditLogsResource from "@/components/admin/audit-logs-resource";
export default function Page() { return <AuditLogsResource />; }
+2 -24
View File
@@ -1,27 +1,5 @@
"use client";
import { useSearchParams } from "next/navigation";
import AuditLogsResource from "@/components/admin/audit-logs-resource";
import CodesResource from "@/components/admin/codes-resource";
import ConsultationsResource from "@/components/admin/consultations-resource";
import CreditTransactionsResource from "@/components/admin/credit-transactions-resource";
import UsersResource from "@/components/admin/users-resource";
const resourceComponents = {
codes: CodesResource,
users: UsersResource,
"credit-transactions": CreditTransactionsResource,
consultations: ConsultationsResource,
"audit-logs": AuditLogsResource,
} as const;
export default function AdminResourcesPage() {
const requested = useSearchParams().get("resource") ?? "codes";
const Resource = resourceComponents[
requested in resourceComponents
? requested as keyof typeof resourceComponents
: "codes"
];
return <Resource />;
export default function AdminCodesPage() {
return <CodesResource />;
}
@@ -0,0 +1,2 @@
import ConsultationsResource from "@/components/admin/consultations-resource";
export default function Page() { return <ConsultationsResource />; }
@@ -0,0 +1,2 @@
import CreditTransactionsResource from "@/components/admin/credit-transactions-resource";
export default function Page() { return <CreditTransactionsResource />; }
@@ -0,0 +1,2 @@
import UsersResource from "@/components/admin/users-resource";
export default function Page() { return <UsersResource />; }
@@ -0,0 +1,2 @@
import FeatureFlagsManagement from "@/components/admin/feature-flags-management";
export default function Page() { return <FeatureFlagsManagement />; }
@@ -0,0 +1,2 @@
import { ModelReleasesResource } from "@/components/admin/billing-operations-resources";
export default function Page() { return <ModelReleasesResource />; }
+2
View File
@@ -0,0 +1,2 @@
import ModelManagement from "@/components/admin/model-management";
export default function Page() { return <ModelManagement />; }
+2
View File
@@ -0,0 +1,2 @@
import { OrdersResource } from "@/components/admin/billing-operations-resources";
export default function Page() { return <OrdersResource />; }
+2
View File
@@ -0,0 +1,2 @@
import ProductManagement from "@/components/admin/product-management";
export default function Page() { return <ProductManagement />; }
+2
View File
@@ -0,0 +1,2 @@
import RolesResource from "@/components/admin/roles-resource";
export default function Page() { return <RolesResource />; }
+5
View File
@@ -0,0 +1,5 @@
import MfaSecurity from "@/components/admin/mfa-security";
export default function Page() {
return <MfaSecurity />;
}
@@ -0,0 +1,2 @@
import { SubscriptionsResource } from "@/components/admin/billing-operations-resources";
export default function Page() { return <SubscriptionsResource />; }
+2
View File
@@ -0,0 +1,2 @@
import { UsageResource } from "@/components/admin/billing-operations-resources";
export default function Page() { return <UsageResource />; }
+3 -31
View File
@@ -1,33 +1,5 @@
"use client";
import { redirect } from "next/navigation";
import Link from "next/link";
import { FormEvent, useEffect, useState } from "react";
type AdminUser = { userId: string | null; email: string | null; createdAt: string | null; source: "env" | "database" };
async function loadUsers(): Promise<AdminUser[]> {
const response = await fetch("/api/admin/users", { cache: "no-store" });
const payload = await response.json().catch(() => null);
if (!response.ok) throw new Error(payload?.error || "暂时无法读取管理员列表");
return payload.users;
}
export default function AdminUsersPage() {
const [users, setUsers] = useState<AdminUser[]>([]);
const [email, setEmail] = useState("");
const [error, setError] = useState("");
useEffect(() => { void loadUsers().then(setUsers).catch((caught) => setError(caught.message)); }, []);
async function add(event: FormEvent) {
event.preventDefault(); setError("");
const response = await fetch("/api/admin/users", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email }) });
const payload = await response.json().catch(() => null);
if (!response.ok) { setError(payload?.error || "添加失败"); return; }
setEmail(""); setUsers(await loadUsers());
}
async function revoke(userId: string) {
const response = await fetch("/api/admin/users", { method: "DELETE", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId }) });
if (!response.ok) { const payload = await response.json().catch(() => null); setError(payload?.error || "撤销失败"); return; }
setUsers(await loadUsers());
}
return <main className="standalone-page admin-page"><header className="admin-header"><h1></h1><Link className="button-secondary" href="/admin/codes"></Link></header><div className="admin-scroll"><section className="admin-section"><div className="section-title"><div><h2></h2><p> Supabase </p></div></div><form className="code-form" onSubmit={add}><label className="note-field"><span></span><input type="email" required value={email} onChange={(event) => setEmail(event.target.value)} /></label><button className="button-primary" type="submit"></button></form>{error && <p className="form-error" role="alert">{error}</p>}</section><section className="admin-section"><div className="section-title"><div><h2></h2><p>{users.length} </p></div></div><div className="admin-table-wrap"><table className="admin-table"><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody>{users.map((user) => <tr key={`${user.source}-${user.userId ?? user.email}`}><td>{user.email || "—"}</td><td>{user.source === "env" ? "环境配置" : "后台配置"}</td><td>{user.createdAt ? new Date(user.createdAt).toLocaleString("zh-CN") : "—"}</td><td>{user.userId && <button className="button-secondary" type="button" onClick={() => void revoke(user.userId!)}></button>}</td></tr>)}</tbody></table></div></section></div></main>;
export default function LegacyUsersPage() {
redirect("/admin/customers");
}