feat: ship enterprise control and desktop governance

This commit is contained in:
AI Bot
2026-05-11 14:59:26 +08:00
parent 0757d07521
commit a311280238
285 changed files with 48574 additions and 2428 deletions

View File

@@ -0,0 +1,118 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import clsx from "clsx";
type SessionSummary = {
sessionId: string;
account: string;
role: string;
displayName: string;
loginMethod: string;
createdAt: string;
expiresAt: string;
lastSeenAt: string;
current: boolean;
};
function formatTime(value: string) {
return new Date(value).toLocaleString("zh-CN", { hour12: false });
}
export function SessionManagementClient({ initialSessions }: { initialSessions: SessionSummary[] }) {
const router = useRouter();
const [sessions, setSessions] = useState(initialSessions);
const [busySessionId, setBusySessionId] = useState("");
const [message, setMessage] = useState("");
async function refresh() {
const response = await fetch("/api/v1/auth/sessions", { cache: "no-store" });
const result = (await response.json()) as { ok: boolean; sessions?: SessionSummary[]; message?: string };
if (!response.ok || !result.ok) {
throw new Error(result.message ?? "刷新失败");
}
setSessions(result.sessions ?? []);
}
async function revoke(sessionId: string, current: boolean) {
setBusySessionId(sessionId);
setMessage("");
try {
const response = await fetch("/api/v1/auth/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "revoke_session", sessionId }),
});
const result = (await response.json()) as { ok: boolean; message?: string };
if (!response.ok || !result.ok) {
throw new Error(result.message ?? "撤销失败");
}
if (current) {
router.replace("/auth/login");
router.refresh();
return;
}
await refresh();
setMessage("会话已撤销。");
} catch (error) {
setMessage(error instanceof Error ? error.message : "撤销失败");
} finally {
setBusySessionId("");
}
}
return (
<section className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] leading-5 text-[#8C8C8C]">
</div>
<div className="mt-4 space-y-3">
{sessions.length === 0 ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[13px] text-[#8C8C8C]">
</div>
) : (
sessions.map((session) => (
<div key={session.sessionId} className="rounded-2xl bg-[#F7F8FA] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<div className="truncate text-[14px] font-semibold text-[#111111]">
{session.displayName || session.account}
</div>
{session.current ? (
<span className="rounded-full bg-[#DFF4E8] px-2 py-0.5 text-[10px] font-semibold text-[#215B39]">
</span>
) : null}
</div>
<div className="mt-1 text-[12px] leading-5 text-[#57606A]">
{session.account} · {session.loginMethod === "code" ? "验证码" : "账号密码"}
<br />
{formatTime(session.lastSeenAt)}
<br />
{formatTime(session.expiresAt)}
</div>
</div>
<button
type="button"
onClick={() => void revoke(session.sessionId, session.current)}
disabled={busySessionId === session.sessionId}
className={clsx(
"shrink-0 rounded-full px-3 py-2 text-[12px] font-semibold",
session.current ? "bg-[#FF3B30] text-white" : "bg-white text-[#FF3B30]",
)}
>
{busySessionId === session.sessionId ? "处理中" : "撤销"}
</button>
</div>
</div>
))
)}
</div>
{message ? <div className="mt-3 text-[12px] text-[#57606A]">{message}</div> : null}
</section>
);
}