- Added dashboard functionality at /status with Basic Auth using DASHBOARD_PASSWORD. - Created server-side event logging to track catalog and university status. - Implemented traffic metrics tracking and display on the dashboard. - Enhanced API tests to cover new dashboard features and authentication. - Introduced CSS and JS for dashboard styling and functionality. - Added tests for dashboard access control and data retrieval.
46 lines
2.1 KiB
JavaScript
46 lines
2.1 KiB
JavaScript
import { createHash, timingSafeEqual } from "node:crypto";
|
|
|
|
// The dashboard's password (DASHBOARD_PASSWORD), asked by the browser's own sign-in window
|
|
// (HTTP Basic). Without it set, the dashboard is off. Ten wrong passwords from one address lock
|
|
// it out for ten minutes.
|
|
|
|
const PASSWORD = process.env.DASHBOARD_PASSWORD || "";
|
|
const MAX_FAILURES = 10;
|
|
const LOCKOUT_MS = 10 * 60 * 1000;
|
|
|
|
export const dashboardEnabled = Boolean(PASSWORD);
|
|
|
|
/** Compares in constant time: the digests have equal length whatever was typed. */
|
|
const same = (a, b) => timingSafeEqual(createHash("sha256").update(a).digest(), createHash("sha256").update(b).digest());
|
|
|
|
export function dashboardAuth() {
|
|
const failures = new Map();
|
|
return (req, res, next) => {
|
|
// Status pages are nobody's search result and nobody's cache.
|
|
res.set({ "Cache-Control": "no-store", "X-Robots-Tag": "noindex" });
|
|
if (!PASSWORD)
|
|
return res.status(404).type("text/plain; charset=utf-8").send("Дашборд выключен: задайте DASHBOARD_PASSWORD.");
|
|
const now = Date.now();
|
|
for (const [ip, f] of failures) if (now - f.start >= LOCKOUT_MS) failures.delete(ip);
|
|
const tries = failures.get(req.ip);
|
|
if (tries && tries.count >= MAX_FAILURES) {
|
|
res.set("Retry-After", String(Math.ceil((tries.start + LOCKOUT_MS - now) / 1000)));
|
|
return res.status(429).type("text/plain; charset=utf-8").send("Слишком много неверных паролей. Попробуйте через 10 минут.");
|
|
}
|
|
const [scheme, value = ""] = (req.get("Authorization") || "").split(" ");
|
|
if (scheme === "Basic") {
|
|
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
// Any user name; only the password counts.
|
|
if (same(decoded.slice(decoded.indexOf(":") + 1), PASSWORD)) {
|
|
failures.delete(req.ip);
|
|
return next();
|
|
}
|
|
const f = tries || { count: 0, start: now };
|
|
f.count++;
|
|
failures.set(req.ip, f);
|
|
}
|
|
res.set("WWW-Authenticate", 'Basic realm="PSU schedule status", charset="UTF-8"');
|
|
res.status(401).type("text/plain; charset=utf-8").send("Нужен пароль дашборда.");
|
|
};
|
|
}
|