Files
boss/src/components/session-management-client.tsx

119 lines
4.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 { 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>
);
}