feat: implement dashboard with authentication and event logging

- 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.
This commit is contained in:
2026-10-21 00:04:33 +03:00
parent de72d003b8
commit 3ae7b38fca
15 changed files with 1216 additions and 74 deletions

54
test/status.test.js Normal file
View File

@@ -0,0 +1,54 @@
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { startApi } from "./support/nextcloud.js";
// The dashboard at /status: the browser's sign-in window, a lockout after ten wrong passwords,
// the page under the strict CSP, and its data.
let api;
before(async () => {
api = await startApi({ DASHBOARD_PASSWORD: "пароль-123" });
});
after(() => api?.stop());
const basic = (user, password) => ({ Authorization: "Basic " + Buffer.from(`${user}:${password}`).toString("base64") });
const get = (path, headers = {}) => fetch(api.base + path, { headers });
test("Дашборд: без пароля — окно входа браузера", async () => {
const response = await get("/status/");
assert.equal(response.status, 401);
assert.match(response.headers.get("www-authenticate"), /^Basic realm=/);
assert.equal(response.headers.get("cache-control"), "no-store");
assert.equal(response.headers.get("x-robots-tag"), "noindex");
assert.equal((await get("/status/data.json")).status, 401);
});
test("Дашборд: с паролем — страница, её файлы и данные", async () => {
const auth = basic("любой", "пароль-123");
const page = await get("/status/", auth);
assert.equal(page.status, 200);
const html = await page.text();
assert.match(html, /<script type="module" src="status\.js"><\/script>/);
// Nothing inline: the strict CSP allows only the server's own files.
assert.doesNotMatch(html, /<script>|style=/);
assert.match(page.headers.get("content-security-policy"), /script-src 'self'/);
assert.equal((await get("/status/status.js", auth)).status, 200);
assert.equal((await get("/status/status.css", auth)).status, 200);
await get("/api/groups?q=23");
const data = await (await get("/status/data.json", auth)).json();
assert.equal(typeof data.server.apiVersion, "number");
assert.ok(Array.isArray(data.server.waiting));
assert.ok(data.catalog.files > 0);
assert.equal(data.directory.documents, 2);
assert.equal(data.traffic.series.length, 60);
assert.ok(Array.isArray(data.events));
assert.equal(typeof data.diagnostics.problems, "number");
assert.equal(data.diagnostics.failed.length, 2);
assert.equal(data.telegram.enabled, false);
});
test("Дашборд: десять неверных паролей — блокировка на 10 минут", async () => {
for (let i = 0; i < 10; i++) assert.equal((await get("/status/", basic("admin", "не тот"))).status, 401);
const locked = await get("/status/", basic("admin", "пароль-123"));
assert.equal(locked.status, 429);
assert.ok(Number(locked.headers.get("retry-after")) > 0);
});