feat: add admin auth session expiration handling

Add auth session utilities and event handling in src/lib/auth-session.ts
Integrate auth expiration checks in API request and uploadMediaAsset functions
Update App component to subscribe to auth expired events and update auth state
Add test suite for the auth session utility function
Adjust maintenance grid CSS layout
Remove unused UI components and imports in StructurePage
This commit is contained in:
duanshuwen
2026-07-11 20:49:12 +08:00
parent 5b089875b3
commit 96c005d479
6 changed files with 46 additions and 21 deletions

View File

@@ -1,12 +1,17 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { getToken } from "./api";
import { AdminShell } from "@/components/admin/AdminShell";
import { subscribeAuthExpired } from "@/lib/auth-session";
import { LoginPage } from "@/pages/login/LoginPage";
export default function App() {
const [authed, setAuthed] = useState(!!getToken());
useEffect(() => {
return subscribeAuthExpired(() => setAuthed(false));
}, []);
if (!authed) {
return <LoginPage onDone={() => setAuthed(true)} />;
}

View File

@@ -1,3 +1,5 @@
import { notifyAuthExpired, shouldExpireAdminSession } from "@/lib/auth-session";
export type Dashboard = {
stats: {
productCount: number;
@@ -319,6 +321,11 @@ export function clearToken() {
window.localStorage.removeItem(TOKEN_KEY);
}
function handleAuthExpired() {
clearToken();
notifyAuthExpired();
}
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const headers = new Headers(options.headers);
headers.set("Content-Type", "application/json");
@@ -331,6 +338,9 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
});
if (!response.ok) {
if (path !== "/api/admin/auth/login" && shouldExpireAdminSession(response.status, Boolean(token))) {
handleAuthExpired();
}
const body = await response.json().catch(() => ({}));
throw new Error(body.message ?? `请求失败:${response.status}`);
}
@@ -387,6 +397,9 @@ export async function uploadMediaAsset(file: File, group = "site-config") {
});
if (!response.ok) {
if (shouldExpireAdminSession(response.status, Boolean(token))) {
handleAuthExpired();
}
const body = await response.json().catch(() => ({}));
throw new Error(body.message ?? `请求失败:${response.status}`);
}

14
src/lib/auth-session.ts Normal file
View File

@@ -0,0 +1,14 @@
export const AUTH_EXPIRED_EVENT = "wonderq-admin-auth-expired";
export function shouldExpireAdminSession(status: number, hadToken: boolean) {
return hadToken && status === 401;
}
export function notifyAuthExpired() {
window.dispatchEvent(new Event(AUTH_EXPIRED_EVENT));
}
export function subscribeAuthExpired(callback: () => void) {
window.addEventListener(AUTH_EXPIRED_EVENT, callback);
return () => window.removeEventListener(AUTH_EXPIRED_EVENT, callback);
}

View File

@@ -3,7 +3,6 @@ import {
ArrowDown,
ArrowUp,
ChevronRight,
Eye,
Plus,
RefreshCcw,
Save,
@@ -666,22 +665,8 @@ export function StructurePage({
</Button>
))}
</aside>
) : (
<section className="page-focus-card">
<span className="focus-kicker"></span>
<h3>{activePage.title}</h3>
<p>{activePage.frontPath}</p>
<small>{activePage.subtitle}</small>
</section>
)}
) : null}
<section className="module-rail">
<div className="front-context">
<Eye size={18} />
<span>
<b>{activePage.frontPath}</b>
<small>{activePage.subtitle}</small>
</span>
</div>
<h3></h3>
{activePage.modules.map((module, index) => (
<div

View File

@@ -474,10 +474,7 @@ textarea {
}
.maintenance-grid.focused-page-grid {
grid-template-columns: minmax(190px, 240px) minmax(240px, 300px) minmax(
320px,
1fr
);
grid-template-columns: minmax(240px, 300px) minmax(320px, 1fr);
}
.page-rail,

View File

@@ -0,0 +1,11 @@
import assert from "node:assert/strict";
import test from "node:test";
import { shouldExpireAdminSession } from "../src/lib/auth-session.js";
test("only token-backed 401 responses expire the admin session", () => {
assert.equal(shouldExpireAdminSession(401, true), true);
assert.equal(shouldExpireAdminSession(401, false), false);
assert.equal(shouldExpireAdminSession(403, true), false);
assert.equal(shouldExpireAdminSession(500, true), false);
});