Add server-side login fallback
This commit is contained in:
parent
b42059f4c8
commit
da5533b5cb
@ -9,10 +9,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<section id="auth-gate" class="auth-gate" hidden>
|
||||
<form id="auth-form" class="auth-card">
|
||||
<form id="auth-form" class="auth-card" action="/auth/login" method="post">
|
||||
<div class="auth-title">输入访问密码</div>
|
||||
<p>云端部署时需要先验证,验证后这台设备会记住登录状态。</p>
|
||||
<input id="auth-password" type="password" autocomplete="current-password" placeholder="访问密码" required>
|
||||
<input id="auth-password" name="password" type="password" autocomplete="current-password" placeholder="访问密码" required>
|
||||
<button id="auth-submit" type="submit">进入系统</button>
|
||||
<div id="auth-message" class="auth-message" aria-live="polite"></div>
|
||||
</form>
|
||||
|
||||
@ -10,10 +10,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<section id="auth-gate" class="auth-gate" hidden>
|
||||
<form id="auth-form" class="auth-card">
|
||||
<form id="auth-form" class="auth-card" action="/auth/login" method="post">
|
||||
<div class="auth-title">输入访问密码</div>
|
||||
<p>云端使用时需要先验证,验证后这台手机会记住登录状态。</p>
|
||||
<input id="auth-password" type="password" autocomplete="current-password" placeholder="访问密码" required>
|
||||
<input id="auth-password" name="password" type="password" autocomplete="current-password" placeholder="访问密码" required>
|
||||
<button id="auth-submit" type="submit">进入手机版</button>
|
||||
<div id="auth-message" class="auth-message" aria-live="polite"></div>
|
||||
</form>
|
||||
|
||||
@ -76,6 +76,15 @@ const server = http.createServer(async (request, response) => {
|
||||
return sendJson(response, 200, { enabled: true, token: ACCESS_TOKEN });
|
||||
}
|
||||
|
||||
if (url.pathname === "/auth/login" && request.method === "POST") {
|
||||
const body = await readFormBody(request);
|
||||
if (!ACCESS_PASSWORD || timingSafeEqualText(String(body.password || ""), ACCESS_PASSWORD)) {
|
||||
if (ACCESS_PASSWORD) response.setHeader("set-cookie", cookieHeader(ACCESS_TOKEN));
|
||||
return sendRedirect(response, body.next || "/");
|
||||
}
|
||||
return sendRedirect(response, "/?auth_error=1");
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/") && !isAuthorizedRequest(request, url)) {
|
||||
return sendAuthRequired(response);
|
||||
}
|
||||
@ -841,6 +850,14 @@ async function readJsonBody(request) {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
async function readFormBody(request) {
|
||||
const chunks = [];
|
||||
for await (const chunk of request) chunks.push(chunk);
|
||||
const content = Buffer.concat(chunks).toString("utf8");
|
||||
const params = new URLSearchParams(content);
|
||||
return Object.fromEntries(params.entries());
|
||||
}
|
||||
|
||||
async function readImageUploadBody(request) {
|
||||
const body = await readJsonBody(request);
|
||||
const type = String(body.type || "").toLowerCase();
|
||||
@ -916,6 +933,12 @@ function sendText(response, status, text, type) {
|
||||
response.end(text);
|
||||
}
|
||||
|
||||
function sendRedirect(response, location) {
|
||||
const safeLocation = String(location || "/").startsWith("/") ? String(location || "/") : "/";
|
||||
response.writeHead(303, { location: safeLocation });
|
||||
response.end();
|
||||
}
|
||||
|
||||
function contentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
return {
|
||||
|
||||
@ -15,6 +15,9 @@ test("server supports optional shared access password authentication", () => {
|
||||
assert.match(server, /HOTNESS_ACCESS_PASSWORD/);
|
||||
assert.match(server, /\/api\/auth\/status/);
|
||||
assert.match(server, /\/api\/auth\/login/);
|
||||
assert.match(server, /\/auth\/login/);
|
||||
assert.match(server, /readFormBody/);
|
||||
assert.match(server, /sendRedirect/);
|
||||
assert.match(server, /isAuthorizedRequest/);
|
||||
assert.match(server, /sendAuthRequired/);
|
||||
assert.match(server, /x-hotness-auth-token/i);
|
||||
@ -22,6 +25,9 @@ test("server supports optional shared access password authentication", () => {
|
||||
|
||||
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"/);
|
||||
assert.match(desktopHtml, /method="post"/);
|
||||
assert.match(desktopHtml, /name="password"/);
|
||||
assert.match(desktopHtml, /id="auth-password"/);
|
||||
assert.match(desktopJs, /HOTNESS_AUTH_TOKEN_KEY/);
|
||||
assert.match(desktopJs, /ensureAccessAuth/);
|
||||
@ -43,6 +49,9 @@ test("desktop login submit is bound before the rest of the app can fail", () =>
|
||||
|
||||
test("mobile page has the same password gate for cloud use", () => {
|
||||
assert.match(mobileHtml, /id="auth-gate"/);
|
||||
assert.match(mobileHtml, /action="\/auth\/login"/);
|
||||
assert.match(mobileHtml, /method="post"/);
|
||||
assert.match(mobileHtml, /name="password"/);
|
||||
assert.match(mobileHtml, /id="auth-password"/);
|
||||
assert.match(mobileJs, /HOTNESS_AUTH_TOKEN_KEY/);
|
||||
assert.match(mobileJs, /ensureAccessAuth/);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user