From b42059f4c805af14b2ed24c78cd0929a7f5b0b25 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 14 May 2026 19:59:17 +0800 Subject: [PATCH] Fix ranking auth during cloud login --- public/app.js | 1 + public/mobile.js | 1 + public/rankings.js | 45 +++++++++++++++++++++++++----------- test/access-password.test.js | 13 +++++++++++ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/public/app.js b/public/app.js index 8ea401d..6a9c93a 100644 --- a/public/app.js +++ b/public/app.js @@ -2163,6 +2163,7 @@ async function submitAccessPassword() { const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "访问密码不正确"); if (payload.token) localStorage.setItem(HOTNESS_AUTH_TOKEN_KEY, payload.token); + window.dispatchEvent(new CustomEvent("hotness:auth-updated")); if (authPassword) authPassword.value = ""; setAuthMessage("登录成功,正在进入..."); hideAuthGate(); diff --git a/public/mobile.js b/public/mobile.js index 9e7fbbc..4a1d1a4 100644 --- a/public/mobile.js +++ b/public/mobile.js @@ -759,6 +759,7 @@ async function submitAccessPassword() { const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "访问密码不正确"); if (payload.token) localStorage.setItem(HOTNESS_AUTH_TOKEN_KEY, payload.token); + window.dispatchEvent(new CustomEvent("hotness:auth-updated")); if (authPassword) authPassword.value = ""; setAuthMessage("登录成功,正在进入..."); hideAuthGate(); diff --git a/public/rankings.js b/public/rankings.js index a57d433..8881f0c 100644 --- a/public/rankings.js +++ b/public/rankings.js @@ -1,3 +1,4 @@ +const HOTNESS_AUTH_TOKEN_KEY = "video-hotness-auth-token-v1"; const PLATFORM_LABELS = { tencent: "腾讯视频", youku: "优酷", iqiyi: "爱奇艺", mgtv: "芒果TV" }; const TYPE_LABELS = { animation: "动画", education: "早教", song: "儿歌", toy: "玩具", movie: "电影", other: "其他" }; const SOURCE_LABELS = { new: "新片", recommend: "推荐", rank: "榜单", hot: "热播", channel: "频道" }; @@ -21,19 +22,26 @@ const state = { }; const root = document.querySelector("#ranking-radar"); -if (root) init(); +if (root) { + window.addEventListener("hotness:auth-updated", () => init()); + init(); +} async function init() { render(); - const [defaults, latest] = await Promise.all([ - apiGet("/api/rankings/default-sources"), - apiGet("/api/kids-trends/latest"), - refreshPrograms(), - ]); - state.defaults = defaults.sources || []; - if (latest.trend?.results?.length) { - state.trendResults = latest.trend.results || []; - state.message = `已恢复上次上新趋势:${formatTime(latest.trend.captured_at)},采集 ${latest.trend.collected_count || state.trendResults.length} 个节目`; + try { + const [defaults, latest] = await Promise.all([ + apiGet("/api/rankings/default-sources"), + apiGet("/api/kids-trends/latest"), + refreshPrograms(), + ]); + state.defaults = defaults.sources || []; + if (latest.trend?.results?.length) { + state.trendResults = latest.trend.results || []; + state.message = `已恢复上次上新趋势:${formatTime(latest.trend.captured_at)},采集 ${latest.trend.collected_count || state.trendResults.length} 个节目`; + } + } catch (error) { + state.message = error.requiresAuth ? "请先输入访问密码" : error.message; } render(); } @@ -386,25 +394,34 @@ function options(map, selected = "") { } async function apiGet(path) { - const response = await fetch(path); + const response = await fetch(path, { headers: authHeaders() }); return parseApiResponse(response); } async function apiPost(path, payload) { const response = await fetch(path, { method: "POST", - headers: { "content-type": "application/json" }, + headers: { "content-type": "application/json", ...authHeaders() }, body: JSON.stringify(payload), }); return parseApiResponse(response); } async function parseApiResponse(response) { - const data = await response.json(); - if (!response.ok) throw new Error(data.error || "request failed"); + const data = await response.json().catch(() => ({})); + if (!response.ok) { + const error = new Error(data.error || "request failed"); + error.requiresAuth = Boolean(data.requires_auth || response.status === 401); + throw error; + } return data; } +function authHeaders() { + const token = localStorage.getItem(HOTNESS_AUTH_TOKEN_KEY) || ""; + return token ? { "x-hotness-auth-token": token } : {}; +} + function formatTime(value) { if (!value) return ""; const date = new Date(value); diff --git a/test/access-password.test.js b/test/access-password.test.js index 71c7d04..49e83b3 100644 --- a/test/access-password.test.js +++ b/test/access-password.test.js @@ -9,6 +9,7 @@ const desktopCss = await readFile(new URL("../public/styles.css", import.meta.ur const mobileHtml = await readFile(new URL("../public/mobile.html", import.meta.url), "utf8"); const mobileJs = await readFile(new URL("../public/mobile.js", import.meta.url), "utf8"); const mobileCss = await readFile(new URL("../public/mobile.css", import.meta.url), "utf8"); +const rankingsJs = await readFile(new URL("../public/rankings.js", import.meta.url), "utf8"); test("server supports optional shared access password authentication", () => { assert.match(server, /HOTNESS_ACCESS_PASSWORD/); @@ -60,3 +61,15 @@ test("mobile login submit is bound before normal capture events", () => { assert.ok(authBinding < collectBinding, "auth binding must run before normal app bindings"); assert.ok(authClickBinding < collectBinding, "auth click binding must run before normal app bindings"); }); + +test("ranking radar requests respect the shared cloud login token", () => { + assert.match(rankingsJs, /HOTNESS_AUTH_TOKEN_KEY/); + assert.match(rankingsJs, /authHeaders/); + assert.match(rankingsJs, /x-hotness-auth-token/i); + assert.match(rankingsJs, /requires_auth/); + assert.match(rankingsJs, /hotness:auth-updated/); +}); + +test("desktop login notifies secondary modules after auth succeeds", () => { + assert.match(desktopJs, /hotness:auth-updated/); +});