Files
AgentHub/server/__tests__/server-mode-password-change.test.js
T
nntrivi2001 750d74a2af chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password
- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as
  the host on loopback; a member's omp pane reaches it as a scoped client keyed
  by its pane token (clients file = live omp panes only), runs on
  --model agenthub/<model>, never holds a credential.
- omp catalog: --allow-home, so a workspace at the account's home keeps its
  sessions there (they were recorded under a temp dir and dropped).
- Settings → Appearance → Open Markdown as: Preview (default) or Raw, for
  Files/Changes, the config viewer and a chat's file panel.
- First-login password change and password rules, avatar URLs, Artifacts
  "Mine" includes installed shared skills/plugins, a member's own hook pages.
2026-10-22 10:04:30 +07:00

182 lines
7.0 KiB
JavaScript

/**
* @file server-mode-password-change.test.js
* @description A password an admin GIVES (Users page create or reset) may be
* anything non-empty, and the account must then pick its own before anything
* else: its login reaches only the `/me` reads and `POST /api/me/password`
* (EPWCHANGE), opens no WebSocket, and its own password must pass every rule
* (EWEAKPASSWORD with the failed `rules`, never the current one). Also: the
* last login survives a logout (`users.last_login_at`), the last enabled
* admin can never be demoted or disabled (ELASTADMIN, the CLI's path
* included) and old audit entries are pruned (`AGENTHUB_AUDIT_DAYS`).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "server-mode-password-change-"));
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
process.env.DASHBOARD_LIVENESS_PROBE = "0";
const { createApp, startServer } = require("../index");
const { db } = require("../db");
const auth = require("../lib/auth");
let server;
let BASE;
const BOSS_PASS = "Admin-pass-2026";
async function call(method, p, { cookie, body } = {}) {
const res = await fetch(BASE + p, {
method,
headers: { "Content-Type": "application/json", ...(cookie ? { cookie } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
return { status: res.status, body: await res.json().catch(() => null) };
}
async function login(username, password) {
const res = await fetch(BASE + "/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (res.status !== 200) return null;
return { cookie: res.headers.get("set-cookie").split(";")[0], user: (await res.json()).user };
}
before(async () => {
process.env.AGENTHUB_MODE = "server";
server = await startServer(createApp(), 0);
BASE = `http://127.0.0.1:${server.address().port}`;
await auth.createUser("boss", BOSS_PASS, "admin");
});
after(() => {
delete process.env.AGENTHUB_MODE;
delete process.env.AGENTHUB_AUDIT_DAYS;
server?.close();
fs.rmSync(TMP, { recursive: true, force: true });
});
describe("a password an admin gave", () => {
it("is accepted weak, then the user must pick a strong one of their own", async () => {
const boss = (await login("boss", BOSS_PASS)).cookie;
const made = await call("POST", "/api/users", {
cookie: boss,
body: { username: "neo", password: "123", currentPassword: BOSS_PASS },
});
assert.equal(made.status, 201);
assert.equal(made.body.user.mustChangePassword, true);
const neo = await login("neo", "123");
assert.equal(neo.user.mustChangePassword, true);
// Only its own account and the password change, until it has one.
assert.equal(
(await call("GET", "/api/lanes", { cookie: neo.cookie })).body.error.code,
"EPWCHANGE"
);
const patch = await call("PATCH", "/api/me/account", {
cookie: neo.cookie,
body: { displayName: "Neo" },
});
assert.equal(patch.body.error.code, "EPWCHANGE");
assert.equal((await call("GET", "/api/me/account", { cookie: neo.cookie })).status, 200);
const me = await call("GET", "/api/auth/me", { cookie: neo.cookie });
assert.equal(me.body.user.mustChangePassword, true);
const weak = await call("POST", "/api/me/password", {
cookie: neo.cookie,
body: { currentPassword: "123", newPassword: "neo12345" },
});
assert.equal(weak.status, 400);
assert.equal(weak.body.error.code, "EWEAKPASSWORD");
assert.deepEqual(weak.body.error.rules, ["upper", "symbol", "username"]);
const ok = await call("POST", "/api/me/password", {
cookie: neo.cookie,
body: { currentPassword: "123", newPassword: "Green-Tea-42" },
});
assert.equal(ok.status, 200);
assert.equal((await call("GET", "/api/lanes", { cookie: neo.cookie })).status, 200);
assert.equal(
(await call("GET", "/api/auth/me", { cookie: neo.cookie })).body.user.mustChangePassword,
false
);
});
it("an admin's reset flags the account again; their own new password must pass every rule", async () => {
const boss = (await login("boss", BOSS_PASS)).cookie;
const neoId = auth.findUser("neo").id;
const reset = await call("PATCH", `/api/users/${neoId}`, {
cookie: boss,
body: { password: "abc", currentPassword: BOSS_PASS },
});
assert.equal(reset.status, 200);
assert.equal(reset.body.user.mustChangePassword, true);
assert.equal((await login("neo", "abc")).user.mustChangePassword, true);
const bossId = auth.findUser("boss").id;
const own = (password) =>
call("PATCH", `/api/users/${bossId}`, {
cookie: boss,
body: { password, currentPassword: BOSS_PASS },
});
assert.equal((await own("123")).body.error.code, "EWEAKPASSWORD");
const same = await own(BOSS_PASS);
assert.equal(same.body.error.code, "EWEAKPASSWORD");
assert.deepEqual(same.body.error.rules, ["reused"]);
const done = await own("Blue-Sky-77!");
assert.equal(done.status, 200);
assert.equal(done.body.user.mustChangePassword, false);
});
});
describe("account bookkeeping", () => {
it("keeps the last login after the user signs out", async () => {
const pass = "Blue-Sky-77!";
const s = await login("boss", pass);
await call("POST", "/api/auth/logout", { cookie: s.cookie });
const id = auth.findUser("boss").id;
db.prepare("DELETE FROM auth_sessions WHERE user_id = ?").run(id);
const stats = auth.loginStats(id);
assert.equal(stats.logins, 0);
assert.ok(stats.lastLoginAt, "the last login is still known");
assert.equal(auth.allLoginStats().get(id).lastLoginAt, stats.lastLoginAt);
});
it("never demotes, disables or deletes the last enabled admin", () => {
for (const fn of [
() => auth.setRole("boss", "developer"),
() => auth.setDisabled("boss", true),
() => auth.deleteUser("boss"),
]) {
assert.throws(fn, { code: "ELASTADMIN" });
}
assert.equal(auth.findUser("boss").role, "admin");
assert.equal(auth.findUser("boss").disabled, 0);
});
it("prunes audit entries older than AGENTHUB_AUDIT_DAYS", () => {
const { pruneAudit } = require("../lib/audit");
const insert = db.prepare(
"INSERT INTO audit_log (user_id, owner_id, method, path, at) VALUES (1, 1, 'GET', ?, ?)"
);
insert.run("/old", new Date(Date.now() - 40 * 864e5).toISOString());
insert.run("/new", new Date().toISOString());
process.env.AGENTHUB_AUDIT_DAYS = "0";
assert.equal(pruneAudit({ force: true }), 0, "0 keeps everything");
process.env.AGENTHUB_AUDIT_DAYS = "30";
assert.ok(pruneAudit({ force: true }) >= 1);
const paths = db
.prepare("SELECT path FROM audit_log")
.all()
.map((r) => r.path);
assert.ok(!paths.includes("/old"));
assert.ok(paths.includes("/new"));
});
});