Add access token fallback for cloud login

This commit is contained in:
Codex 2026-05-14 20:26:02 +08:00
parent 7bf5bcae43
commit e1a80f5527
4 changed files with 47 additions and 1 deletions

View File

@ -609,6 +609,7 @@ dutyRunNow?.addEventListener("click", () => {
runDutyNow(); runDutyNow();
}); });
consumeRedirectedAccessToken();
initializeApp(); initializeApp();
document.addEventListener("hotness:programs-changed", refreshPrograms); document.addEventListener("hotness:programs-changed", refreshPrograms);
@ -617,6 +618,17 @@ async function initializeApp() {
startApp(); startApp();
} }
function consumeRedirectedAccessToken() {
const params = new URLSearchParams(window.location.search);
const token = params.get("access_token");
if (!token) return;
localStorage.setItem(HOTNESS_AUTH_TOKEN_KEY, token);
params.delete("access_token");
const search = params.toString();
const cleanUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`;
history.replaceState(null, "", cleanUrl || "/");
}
function startApp() { function startApp() {
if (appStarted) return; if (appStarted) return;
appStarted = true; appStarted = true;

View File

@ -175,6 +175,7 @@ window.addEventListener("appinstalled", () => {
updateInstallPrompt("installed"); updateInstallPrompt("installed");
}); });
consumeRedirectedAccessToken();
initializeApp(); initializeApp();
async function initializeApp() { async function initializeApp() {
@ -182,6 +183,17 @@ async function initializeApp() {
startApp(); startApp();
} }
function consumeRedirectedAccessToken() {
const params = new URLSearchParams(window.location.search);
const token = params.get("access_token");
if (!token) return;
localStorage.setItem(HOTNESS_AUTH_TOKEN_KEY, token);
params.delete("access_token");
const search = params.toString();
const cleanUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`;
history.replaceState(null, "", cleanUrl || "/");
}
async function startApp() { async function startApp() {
if (appStarted) return; if (appStarted) return;
appStarted = true; appStarted = true;

View File

@ -80,7 +80,7 @@ const server = http.createServer(async (request, response) => {
const body = await readFormBody(request); const body = await readFormBody(request);
if (!ACCESS_PASSWORD || timingSafeEqualText(String(body.password || ""), ACCESS_PASSWORD)) { if (!ACCESS_PASSWORD || timingSafeEqualText(String(body.password || ""), ACCESS_PASSWORD)) {
if (ACCESS_PASSWORD) response.setHeader("set-cookie", cookieHeader(ACCESS_TOKEN)); if (ACCESS_PASSWORD) response.setHeader("set-cookie", cookieHeader(ACCESS_TOKEN));
return sendRedirect(response, body.next || "/"); return sendRedirect(response, buildAuthRedirectLocation(body.next || "/", ACCESS_TOKEN));
} }
return sendRedirect(response, "/?auth_error=1"); return sendRedirect(response, "/?auth_error=1");
} }
@ -909,6 +909,14 @@ function cookieHeader(token) {
return `hotness_auth=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}`; return `hotness_auth=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${60 * 60 * 24 * 30}`;
} }
function buildAuthRedirectLocation(location, token) {
const safeLocation = String(location || "/").startsWith("/") ? String(location || "/") : "/";
if (!token) return safeLocation;
const redirectUrl = new URL(safeLocation, "http://video-hotness.local");
redirectUrl.searchParams.set("access_token", token);
return `${redirectUrl.pathname}${redirectUrl.search}${redirectUrl.hash}`;
}
function timingSafeEqualText(left, right) { function timingSafeEqualText(left, right) {
const leftBuffer = Buffer.from(String(left || "")); const leftBuffer = Buffer.from(String(left || ""));
const rightBuffer = Buffer.from(String(right || "")); const rightBuffer = Buffer.from(String(right || ""));

View File

@ -42,6 +42,14 @@ test("desktop login form is not blocked by JavaScript", () => {
assert.doesNotMatch(desktopJs, /async function submitAccessPassword/); assert.doesNotMatch(desktopJs, /async function submitAccessPassword/);
}); });
test("desktop can finish login from a redirected access token", () => {
assert.match(server, /buildAuthRedirectLocation/);
assert.match(server, /access_token/);
assert.match(desktopJs, /consumeRedirectedAccessToken/);
assert.match(desktopJs, /URLSearchParams/);
assert.match(desktopJs, /history\.replaceState/);
});
test("mobile page has the same password gate for cloud use", () => { test("mobile page has the same password gate for cloud use", () => {
assert.match(mobileHtml, /id="auth-gate"/); assert.match(mobileHtml, /id="auth-gate"/);
assert.match(mobileHtml, /action="\/auth\/login"/); assert.match(mobileHtml, /action="\/auth\/login"/);
@ -61,6 +69,12 @@ test("mobile login form is not blocked by JavaScript", () => {
assert.doesNotMatch(mobileJs, /async function submitAccessPassword/); assert.doesNotMatch(mobileJs, /async function submitAccessPassword/);
}); });
test("mobile can finish login from a redirected access token", () => {
assert.match(mobileJs, /consumeRedirectedAccessToken/);
assert.match(mobileJs, /URLSearchParams/);
assert.match(mobileJs, /history\.replaceState/);
});
test("ranking radar requests respect the shared cloud login token", () => { test("ranking radar requests respect the shared cloud login token", () => {
assert.match(rankingsJs, /HOTNESS_AUTH_TOKEN_KEY/); assert.match(rankingsJs, /HOTNESS_AUTH_TOKEN_KEY/);
assert.match(rankingsJs, /authHeaders/); assert.match(rankingsJs, /authHeaders/);