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:
@@ -1,12 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { getToken } from "./api";
|
import { getToken } from "./api";
|
||||||
import { AdminShell } from "@/components/admin/AdminShell";
|
import { AdminShell } from "@/components/admin/AdminShell";
|
||||||
|
import { subscribeAuthExpired } from "@/lib/auth-session";
|
||||||
import { LoginPage } from "@/pages/login/LoginPage";
|
import { LoginPage } from "@/pages/login/LoginPage";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [authed, setAuthed] = useState(!!getToken());
|
const [authed, setAuthed] = useState(!!getToken());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return subscribeAuthExpired(() => setAuthed(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!authed) {
|
if (!authed) {
|
||||||
return <LoginPage onDone={() => setAuthed(true)} />;
|
return <LoginPage onDone={() => setAuthed(true)} />;
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/api.ts
13
src/api.ts
@@ -1,3 +1,5 @@
|
|||||||
|
import { notifyAuthExpired, shouldExpireAdminSession } from "@/lib/auth-session";
|
||||||
|
|
||||||
export type Dashboard = {
|
export type Dashboard = {
|
||||||
stats: {
|
stats: {
|
||||||
productCount: number;
|
productCount: number;
|
||||||
@@ -319,6 +321,11 @@ export function clearToken() {
|
|||||||
window.localStorage.removeItem(TOKEN_KEY);
|
window.localStorage.removeItem(TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAuthExpired() {
|
||||||
|
clearToken();
|
||||||
|
notifyAuthExpired();
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const headers = new Headers(options.headers);
|
const headers = new Headers(options.headers);
|
||||||
headers.set("Content-Type", "application/json");
|
headers.set("Content-Type", "application/json");
|
||||||
@@ -331,6 +338,9 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (path !== "/api/admin/auth/login" && shouldExpireAdminSession(response.status, Boolean(token))) {
|
||||||
|
handleAuthExpired();
|
||||||
|
}
|
||||||
const body = await response.json().catch(() => ({}));
|
const body = await response.json().catch(() => ({}));
|
||||||
throw new Error(body.message ?? `请求失败:${response.status}`);
|
throw new Error(body.message ?? `请求失败:${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -387,6 +397,9 @@ export async function uploadMediaAsset(file: File, group = "site-config") {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (shouldExpireAdminSession(response.status, Boolean(token))) {
|
||||||
|
handleAuthExpired();
|
||||||
|
}
|
||||||
const body = await response.json().catch(() => ({}));
|
const body = await response.json().catch(() => ({}));
|
||||||
throw new Error(body.message ?? `请求失败:${response.status}`);
|
throw new Error(body.message ?? `请求失败:${response.status}`);
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/lib/auth-session.ts
Normal file
14
src/lib/auth-session.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
ArrowDown,
|
ArrowDown,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Eye,
|
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
Save,
|
Save,
|
||||||
@@ -666,22 +665,8 @@ export function StructurePage({
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</aside>
|
</aside>
|
||||||
) : (
|
) : null}
|
||||||
<section className="page-focus-card">
|
|
||||||
<span className="focus-kicker">当前维护页面</span>
|
|
||||||
<h3>{activePage.title}</h3>
|
|
||||||
<p>{activePage.frontPath}</p>
|
|
||||||
<small>{activePage.subtitle}</small>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
<section className="module-rail">
|
<section className="module-rail">
|
||||||
<div className="front-context">
|
|
||||||
<Eye size={18} />
|
|
||||||
<span>
|
|
||||||
<b>{activePage.frontPath}</b>
|
|
||||||
<small>{activePage.subtitle}</small>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h3>页面模块</h3>
|
<h3>页面模块</h3>
|
||||||
{activePage.modules.map((module, index) => (
|
{activePage.modules.map((module, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -474,10 +474,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.maintenance-grid.focused-page-grid {
|
.maintenance-grid.focused-page-grid {
|
||||||
grid-template-columns: minmax(190px, 240px) minmax(240px, 300px) minmax(
|
grid-template-columns: minmax(240px, 300px) minmax(320px, 1fr);
|
||||||
320px,
|
|
||||||
1fr
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-rail,
|
.page-rail,
|
||||||
|
|||||||
11
tests/auth-session.test.ts
Normal file
11
tests/auth-session.test.ts
Normal 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user