diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 9765446..233d2f5 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -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(() => loadSession()); + const [loggingOut, setLoggingOut] = useState(false); const navigate = useNavigate(); + async function handleLogout(): Promise { + if (!session || loggingOut) { + return; + } + + try { + setLoggingOut(true); + await revokeRefreshToken(session.refreshToken); + } catch { + // 登出流程以本地会话清理为最终兜底,避免页面卡在登录态。 + } finally { + clearSession(); + setSession(null); + setLoggingOut(false); + navigate("/login/email", { replace: true }); + } + } + return (
@@ -29,7 +54,22 @@ function App() {
TodoList
- {session ? session.user.email : "未登录"} + {session ? ( +
+ {session.user.email} + +
+ ) : ( + 未登录 + )}
diff --git a/apps/web/src/services/auth-api.ts b/apps/web/src/services/auth-api.ts index 29a014a..5f2919e 100644 --- a/apps/web/src/services/auth-api.ts +++ b/apps/web/src/services/auth-api.ts @@ -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 { 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 { + 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; +}