fix(web): logout-path parity, CSP header, epub display fallback + small review nits
This commit is contained in:
@@ -5,6 +5,9 @@ server {
|
||||
# /etc/resolv.conf 的首个 nameserver 重写本行,兼容 podman aardvark-dns。
|
||||
resolver 127.0.0.11 valid=10s;
|
||||
|
||||
# spec §7 风险接受所假设的 CSP:全部同源,blob:/data: 供 SW/reader 用
|
||||
add_header Content-Security-Policy "default-src 'self'; img-src 'self' blob: data:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' blob: data:; object-src 'none'; frame-src 'self' blob:" always;
|
||||
|
||||
location /api/ {
|
||||
set $api_upstream http://api:8080; # 变量式 → 每次按 DNS 解析,scale 后轮询到新副本(spec §11)
|
||||
proxy_pass $api_upstream; # 无 URI 部分:保留 /api 前缀转发
|
||||
|
||||
@@ -18,8 +18,20 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const qc = useQueryClient();
|
||||
const [token, setTok] = useState<string | null>(() => getToken());
|
||||
|
||||
// 401 到期登出与显式登出同样清理:跨用户数据不得残留在 query 缓存 / SW CacheStorage
|
||||
const clearSession = () => {
|
||||
qc.clear();
|
||||
if (typeof caches !== "undefined") {
|
||||
void caches.delete("booklib-api");
|
||||
void caches.delete("booklib-immutable");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const off = () => setTok(null); // client 的 401 拦截在这里回收 React 状态
|
||||
const off = () => {
|
||||
setTok(null); // client 的 401 拦截在这里回收 React 状态(token 已由 client.ts 清除)
|
||||
clearSession();
|
||||
};
|
||||
window.addEventListener(LOGOUT_EVENT, off);
|
||||
return () => window.removeEventListener(LOGOUT_EVENT, off);
|
||||
}, []);
|
||||
@@ -46,11 +58,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
logout: () => {
|
||||
setToken(null);
|
||||
setTok(null);
|
||||
qc.clear();
|
||||
if (typeof caches !== "undefined") {
|
||||
void caches.delete("booklib-api");
|
||||
void caches.delete("booklib-immutable");
|
||||
}
|
||||
clearSession();
|
||||
},
|
||||
}),
|
||||
[meQ.data, meQ.isPending, meQ.isError, token, qc],
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function AdminUsers() {
|
||||
<h1 className="mb-4 text-lg font-semibold">用户管理</h1>
|
||||
<form onSubmit={submit} className={`${card} mb-6 flex max-w-2xl flex-wrap items-center gap-3 p-4`}>
|
||||
<input className={input} placeholder="用户名" value={u} onChange={(e) => setU(e.target.value)} />
|
||||
<input className={input} placeholder="密码(≥8 位)" value={p} onChange={(e) => setP(e.target.value)} />
|
||||
<input className={input} type="password" placeholder="密码(≥8 位)" value={p} onChange={(e) => setP(e.target.value)} />
|
||||
<select className={input} value={role} onChange={(e) => setRole(e.target.value as "member" | "admin")}>
|
||||
<option value="member">member</option>
|
||||
<option value="admin">admin</option>
|
||||
|
||||
@@ -100,7 +100,9 @@ export default function CbzReader({ book, initialLocator }: ReaderProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (countQ.isPending) return <div className="grid h-full place-items-center text-zinc-500">页索引加载中…</div>;
|
||||
// pages_url 缺失时 query 被 disabled 永久 pending → 只有真正发起了请求才显示加载态
|
||||
if (countQ.isPending && !!book.pages_url)
|
||||
return <div className="grid h-full place-items-center text-zinc-500">页索引加载中…</div>;
|
||||
if (countQ.isError || !book.page_url_fmt)
|
||||
return (
|
||||
<div className="grid h-full place-items-center p-8 text-center text-zinc-500">
|
||||
|
||||
@@ -8,7 +8,7 @@ interface EpubLocation {
|
||||
start?: { cfi?: string; sectionIndex?: number };
|
||||
}
|
||||
interface EpubRendition {
|
||||
display(target?: string): unknown;
|
||||
display(target?: string): Promise<unknown> | unknown;
|
||||
next(): unknown;
|
||||
prev(): unknown;
|
||||
on(ev: "relocated", cb: (loc: EpubLocation) => void): void;
|
||||
@@ -56,12 +56,18 @@ export default function EpubReader({ book, initialLocator }: ReaderProps) {
|
||||
saver.report(cfi ? { cfi } : {}, Math.min(1, pct));
|
||||
});
|
||||
const cfi = typeof initialLocator?.cfi === "string" ? (initialLocator.cfi as string) : undefined;
|
||||
try {
|
||||
r.display(cfi); // cfi 失效(书被替换等)时 epubjs 会抛
|
||||
} catch {
|
||||
r.display();
|
||||
// epubjs 对失效 cfi 以 reject Promise signalling,而非同步 throw → 走 then/catch 回退
|
||||
Promise.resolve(cfi ? r.display(cfi) : r.display())
|
||||
.then(() => setReady(true))
|
||||
.catch(() => {
|
||||
if (!cfi) {
|
||||
setErr("EPUB 无法打开");
|
||||
return;
|
||||
}
|
||||
setReady(true);
|
||||
Promise.resolve(r.display())
|
||||
.then(() => setReady(true))
|
||||
.catch(() => setErr("EPUB 无法打开"));
|
||||
});
|
||||
})().catch((e) => !dead && setErr(e instanceof Error ? e.message : "EPUB 加载失败"));
|
||||
return () => {
|
||||
dead = true;
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export default defineConfig({
|
||||
// ?v={hash} 的封面/CBZ 页/原文件 = 不可变 → cache-first(spec §6.1/§6.3)
|
||||
urlPattern: ({ url }) =>
|
||||
url.pathname.startsWith("/api/books/") &&
|
||||
url.search.includes("v=") &&
|
||||
url.searchParams.has("v") &&
|
||||
(/\/cover$/.test(url.pathname) || /\/pages\/\d+$/.test(url.pathname) || /\/file$/.test(url.pathname)),
|
||||
handler: "CacheFirst",
|
||||
options: { cacheName: "booklib-immutable", expiration: { maxEntries: 3000, purgeOnQuotaError: true } },
|
||||
|
||||
Reference in New Issue
Block a user