Serve password page before protected app

This commit is contained in:
Codex 2026-05-14 20:32:15 +08:00
parent e1a80f5527
commit a69804a163
2 changed files with 68 additions and 0 deletions

View File

@ -89,6 +89,10 @@ const server = http.createServer(async (request, response) => {
return sendAuthRequired(response);
}
if (request.method === "GET" && ACCESS_PASSWORD && isProtectedAppPage(url.pathname) && !isAuthorizedRequest(request, url)) {
return sendLoginPage(response, url);
}
if (url.pathname === "/api/desktop-instance" && request.method === "GET") {
return sendJson(response, 200, {
desktopRoot,
@ -947,6 +951,60 @@ function sendRedirect(response, location) {
response.end();
}
function isProtectedAppPage(pathname) {
return ["/", "/index.html", "/mobile.html"].includes(pathname);
}
function sendLoginPage(response, url) {
const isMobile = url.pathname === "/mobile.html";
const next = `${url.pathname}${url.search}`;
const error = url.searchParams.has("auth_error") ? "访问密码不正确,请重新输入。" : "";
response.writeHead(200, {
"content-type": "text/html; charset=utf-8",
"cache-control": "no-store",
});
response.end(`<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>节目热度采集 - 访问验证</title>
<style>
body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: Arial, "Microsoft YaHei", sans-serif; background: #f3f7f8; color: #18323a; }
form { width: min(360px, calc(100vw - 32px)); background: white; border: 1px solid #cfe0e5; border-radius: 8px; padding: 24px; box-shadow: 0 12px 32px rgba(15, 45, 55, 0.12); }
h1 { margin: 0 0 8px; font-size: 22px; }
p { margin: 0 0 18px; color: #5a6f78; line-height: 1.5; }
input[type="password"] { box-sizing: border-box; width: 100%; height: 42px; border: 1px solid #b9ccd2; border-radius: 6px; padding: 0 12px; font-size: 16px; }
button { width: 100%; height: 42px; margin-top: 14px; border: 0; border-radius: 6px; background: #0f766e; color: white; font-size: 16px; font-weight: 700; cursor: pointer; }
.error { min-height: 20px; margin-top: 12px; color: #b42318; font-size: 14px; }
</style>
</head>
<body>
<form action="/auth/login" method="post">
<h1>输入访问密码</h1>
<p>${isMobile ? "验证后进入手机录入版。" : "验证后进入节目热度采集系统。"}</p>
<input type="hidden" name="next" value="${escapeHtmlAttribute(next)}">
<input name="password" type="password" autocomplete="current-password" placeholder="访问密码" required autofocus>
<button type="submit">进入系统</button>
<div class="error">${escapeHtmlText(error)}</div>
</form>
</body>
</html>`);
}
function escapeHtmlText(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function escapeHtmlAttribute(value) {
return escapeHtmlText(value)
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function contentType(filePath) {
const ext = path.extname(filePath).toLowerCase();
return {

View File

@ -16,6 +16,8 @@ test("server supports optional shared access password authentication", () => {
assert.match(server, /\/api\/auth\/status/);
assert.match(server, /\/api\/auth\/login/);
assert.match(server, /\/auth\/login/);
assert.match(server, /sendLoginPage/);
assert.match(server, /isProtectedAppPage/);
assert.match(server, /readFormBody/);
assert.match(server, /sendRedirect/);
assert.match(server, /isAuthorizedRequest/);
@ -23,6 +25,14 @@ test("server supports optional shared access password authentication", () => {
assert.match(server, /x-hotness-auth-token/i);
});
test("server renders a standalone password page before protected app pages", () => {
assert.match(server, /isProtectedAppPage\(url\.pathname\)/);
assert.match(server, /!isAuthorizedRequest\(request, url\)/);
assert.match(server, /sendLoginPage\(response, url\)/);
assert.match(server, /name="next"/);
assert.match(server, /输入访问密码/);
});
test("desktop page has a password gate and sends auth token with API calls", () => {
assert.match(desktopHtml, /id="auth-gate"/);
assert.match(desktopHtml, /action="\/auth\/login"/);