diff --git a/src/server.js b/src/server.js index e9b3ff5..0146f83 100644 --- a/src/server.js +++ b/src/server.js @@ -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(` + + + + + 节目热度采集 - 访问验证 + + + +
+

输入访问密码

+

${isMobile ? "验证后进入手机录入版。" : "验证后进入节目热度采集系统。"}

+ + + +
${escapeHtmlText(error)}
+
+ +`); +} + +function escapeHtmlText(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function escapeHtmlAttribute(value) { + return escapeHtmlText(value) + .replace(/"/g, """) + .replace(/'/g, "'"); +} + function contentType(filePath) { const ext = path.extname(filePath).toLowerCase(); return { diff --git a/test/access-password.test.js b/test/access-password.test.js index 6faf490..f0b64ff 100644 --- a/test/access-password.test.js +++ b/test/access-password.test.js @@ -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"/);