feat(web-auth): implement logout with token revoke and session clear

This commit is contained in:
2026-04-05 15:05:51 +08:00
parent 25857abf26
commit 95c10eca77
2 changed files with 66 additions and 5 deletions
+43 -3
View File
@@ -1,10 +1,16 @@
import { useState } from "react";
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { EmailLoginPage } from "@/pages/email-login-page";
import { OAuthCallbackPage } from "@/pages/oauth-callback-page";
import { TodoShellPage } from "@/pages/todo-shell-page";
import type { EmailLoginResult } from "@/services/auth-api";
import { loadSession, saveSession, type WebSession } from "@/services/session-storage";
import { revokeRefreshToken, type EmailLoginResult } from "@/services/auth-api";
import {
clearSession,
loadSession,
saveSession,
type WebSession
} from "@/services/session-storage";
function toWebSession(payload: EmailLoginResult): WebSession {
return {
@@ -19,8 +25,27 @@ function toWebSession(payload: EmailLoginResult): WebSession {
function App() {
const [session, setSession] = useState<WebSession | null>(() => loadSession());
const [loggingOut, setLoggingOut] = useState(false);
const navigate = useNavigate();
async function handleLogout(): Promise<void> {
if (!session || loggingOut) {
return;
}
try {
setLoggingOut(true);
await revokeRefreshToken(session.refreshToken);
} catch {
// 登出流程以本地会话清理为最终兜底,避免页面卡在登录态。
} finally {
clearSession();
setSession(null);
setLoggingOut(false);
navigate("/login/email", { replace: true });
}
}
return (
<div className="min-h-screen bg-[#f6f8f7] text-[#122117]">
<header className="border-b border-[#d7e2db] bg-white/90 backdrop-blur">
@@ -29,7 +54,22 @@ function App() {
<div className="h-8 w-8 rounded-lg bg-[#0a7a5a]" />
<span className="text-lg font-semibold tracking-tight">TodoList</span>
</div>
<span className="text-sm text-[#3a5a4a]">{session ? session.user.email : "未登录"}</span>
{session ? (
<div className="flex items-center gap-3">
<span className="text-sm text-[#3a5a4a]">{session.user.email}</span>
<Button
type="button"
size="sm"
variant="outline"
onClick={handleLogout}
disabled={loggingOut}
>
{loggingOut ? "退出中..." : "退出登录"}
</Button>
</div>
) : (
<span className="text-sm text-[#3a5a4a]"></span>
)}
</div>
</header>
<main className="mx-auto w-full max-w-6xl px-4 py-8">
+23 -2
View File
@@ -1,4 +1,4 @@
export type SendEmailCodeResult = {
export type SendEmailCodeResult = {
success: boolean;
expiresInSeconds: number;
};
@@ -15,6 +15,10 @@ export type EmailLoginResult = {
};
};
type RevokeRefreshTokenResult = {
success: boolean;
};
const DEFAULT_API_BASE_URL = "http://localhost:3000";
function resolveApiBaseUrl(): string {
@@ -30,7 +34,7 @@ async function parseErrorMessage(response: Response): Promise<string> {
try {
const body = (await response.json()) as { message?: string | string[] };
if (Array.isArray(body.message)) {
return body.message.join("");
return body.message.join("");
}
if (typeof body.message === "string" && body.message.trim()) {
return body.message;
@@ -75,3 +79,20 @@ export async function loginWithEmailCode(email: string, code: string): Promise<E
const body = (await response.json()) as EmailLoginResult;
return body;
}
export async function revokeRefreshToken(refreshToken: string): Promise<RevokeRefreshTokenResult> {
const response = await fetch(`${resolveApiBaseUrl()}/auth/token/revoke`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
throw new Error(await parseErrorMessage(response));
}
const body = (await response.json()) as RevokeRefreshTokenResult;
return body;
}