Files
AgentHub/server/__tests__/server-mode-user-admin.test.js
T
nntrivi2001 a9c71cd905 feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)
Usage panel (status bar):
- every account shows its plan (Claude subscriptionType, Codex
  chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier)
- new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity
  (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs
  agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's
  own file, sent only to its own host while valid, never refreshed
- in-flight quota reads are shared between tabs

AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added;
Gemini CLI, Hermes and OpenClaw removed.

Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat
on an editor selection, Settings in three groups, JetBrains-style Changes
panel (ticked files/hunks, ignore, blame, file history), terminal font.

--no-verify: :3000 is taken by another dev server (lane-detect tests);
both suites run by hand — server 2636/2639 (the 3 are :3000), client
1131/1131.
2026-10-08 12:22:14 +07:00

261 lines
9.5 KiB
JavaScript

/**
* @file server-mode-user-admin.test.js
* @description Managing accounts from the dashboard (Users page): only an
* admin may create a user, change a role, disable/enable, reset a password or
* give a workspace, every change needs the admin's OWN password again
* (EREAUTH, throttled), an admin can never demote or disable themselves, no
* answer carries a password hash, each change lands in the audit log, and in
* local mode — no login at all — the routes refuse.
* @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-user-admin-"));
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 ids = {};
const BOSS_PASS = "admin-pass-1";
async function call(method, p, { cookie, body, origin } = {}) {
const res = await fetch(BASE + p, {
method,
headers: {
"Content-Type": "application/json",
...(cookie ? { cookie } : {}),
...(origin ? { origin } : {}),
},
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 }),
});
return res.status === 200 ? res.headers.get("set-cookie").split(";")[0] : null;
}
before(async () => {
process.env.AGENTHUB_MODE = "server";
server = await startServer(createApp(), 0);
BASE = `http://127.0.0.1:${server.address().port}`;
ids.boss = (await auth.createUser("boss", BOSS_PASS, "admin")).id;
ids.ana = (await auth.createUser("ana", "analyst-pass-1", "analyst")).id;
ids.mia = (await auth.createUser("mia", "member-pass-1", "member")).id;
ids.lane = db
.prepare("INSERT INTO lanes (cwd, tool) VALUES ('/w/ownerless', 'claude')")
.run().lastInsertRowid;
});
after(() => {
delete process.env.AGENTHUB_MODE;
server?.close();
fs.rmSync(TMP, { recursive: true, force: true });
});
describe("managing accounts from the dashboard", () => {
it("lets an admin create a user, with their own password again", async () => {
const boss = await login("boss", BOSS_PASS);
const r = await call("POST", "/api/users", {
cookie: boss,
body: {
username: "neo",
password: "neo-pass-123",
role: "member",
currentPassword: BOSS_PASS,
},
});
assert.equal(r.status, 201);
assert.deepEqual(
{ username: r.body.user.username, role: r.body.user.role },
{ username: "neo", role: "member" }
);
assert.ok(!JSON.stringify(r.body).includes("scrypt"), "no password hash");
assert.ok(await login("neo", "neo-pass-123"), "the new user can log in");
const dup = await call("POST", "/api/users", {
cookie: boss,
body: { username: "neo", password: "neo-pass-123", currentPassword: BOSS_PASS },
});
assert.equal(dup.status, 409);
assert.equal(dup.body.error.code, "EEXISTS");
const short = await call("POST", "/api/users", {
cookie: boss,
body: { username: "zed", password: "short", currentPassword: BOSS_PASS },
});
assert.equal(short.status, 400);
assert.equal(short.body.error.code, "EBADPASSWORD");
});
it("refuses without the admin's password, or with a wrong one", async () => {
const boss = await login("boss", BOSS_PASS);
for (const currentPassword of [undefined, "not-the-password"]) {
const r = await call("POST", "/api/users", {
cookie: boss,
body: { username: "eve", password: "eve-pass-123", currentPassword },
});
assert.equal(r.status, 403);
assert.equal(r.body.error.code, "EREAUTH");
}
assert.equal(auth.findUser("eve"), undefined);
});
it("is an admin's only: an analyst is read-only, a member never reaches it", async () => {
const body = { username: "eve", password: "eve-pass-123", currentPassword: "analyst-pass-1" };
const ana = await call("POST", "/api/users", {
cookie: await login("ana", "analyst-pass-1"),
body,
});
assert.equal(ana.status, 403);
const mia = await call("POST", "/api/users", {
cookie: await login("mia", "member-pass-1"),
body: { ...body, currentPassword: "member-pass-1" },
});
assert.equal(mia.status, 403);
assert.equal(auth.findUser("eve"), undefined);
});
it("refuses a write from another origin", async () => {
const r = await call("POST", "/api/users", {
cookie: await login("boss", BOSS_PASS),
origin: "http://evil.example",
body: { username: "eve", password: "eve-pass-123", currentPassword: BOSS_PASS },
});
assert.equal(r.status, 403);
assert.equal(auth.findUser("eve"), undefined);
});
it("changes a role, disables and enables, and resets a password", async () => {
const boss = await login("boss", BOSS_PASS);
const neo = auth.findUser("neo");
const role = await call("PATCH", `/api/users/${neo.id}`, {
cookie: boss,
body: { role: "analyst", currentPassword: BOSS_PASS },
});
assert.equal(role.status, 200);
assert.equal(auth.findUser("neo").role, "analyst");
const neoLogin = await login("neo", "neo-pass-123");
const off = await call("PATCH", `/api/users/${neo.id}`, {
cookie: boss,
body: { disabled: true, currentPassword: BOSS_PASS },
});
assert.equal(off.status, 200);
assert.equal(off.body.user.disabled, true);
assert.equal((await call("GET", "/api/sessions", { cookie: neoLogin })).status, 401);
assert.equal(await login("neo", "neo-pass-123"), null, "a disabled user cannot log in");
await call("PATCH", `/api/users/${neo.id}`, {
cookie: boss,
body: { disabled: false, currentPassword: BOSS_PASS },
});
const pw = await call("POST", `/api/users/${neo.id}/password`, {
cookie: boss,
body: { password: "neo-new-pass-9", currentPassword: BOSS_PASS },
});
assert.equal(pw.status, 200);
assert.equal(await login("neo", "neo-pass-123"), null);
assert.ok(await login("neo", "neo-new-pass-9"));
const bad = await call("PATCH", `/api/users/${neo.id}`, {
cookie: boss,
body: { role: "root", currentPassword: BOSS_PASS },
});
assert.equal(bad.status, 400);
const none = await call("PATCH", "/api/users/9999", {
cookie: boss,
body: { role: "member", currentPassword: BOSS_PASS },
});
assert.equal(none.status, 404);
});
it("never lets an admin demote or disable themselves", async () => {
const boss = await login("boss", BOSS_PASS);
for (const body of [{ role: "member" }, { disabled: true }]) {
const r = await call("PATCH", `/api/users/${ids.boss}`, {
cookie: boss,
body: { ...body, currentPassword: BOSS_PASS },
});
assert.equal(r.status, 409);
assert.equal(r.body.error.code, "ESELF");
}
assert.equal(auth.findUser("boss").role, "admin");
});
it("gives a workspace to a user", async () => {
const boss = await login("boss", BOSS_PASS);
const r = await call("POST", `/api/users/${ids.mia}/workspaces`, {
cookie: boss,
body: { laneId: ids.lane, currentPassword: BOSS_PASS },
});
assert.equal(r.status, 200);
assert.equal(
db.prepare("SELECT owner_id FROM lanes WHERE id = ?").get(ids.lane).owner_id,
ids.mia
);
const none = await call("POST", `/api/users/${ids.mia}/workspaces`, {
cookie: boss,
body: { laneId: 9999, currentPassword: BOSS_PASS },
});
assert.equal(none.status, 404);
});
it("writes every change to the audit log", async () => {
const boss = await login("boss", BOSS_PASS);
const entries = (await call("GET", "/api/users/audit", { cookie: boss })).body.entries;
const mine = entries.filter((e) => e.reader === "boss" && e.method !== "GET");
const paths = mine.map((e) => e.path).join("\n");
assert.match(paths, /create neo/);
assert.match(paths, /role analyst/);
assert.match(paths, /disable/);
assert.match(paths, /password reset/);
assert.match(paths, new RegExp(`workspace ${ids.lane}`));
assert.ok(!paths.includes("neo-new-pass-9"), "never the password itself");
});
it("throttles wrong confirmation passwords", async () => {
const boss = await login("boss", BOSS_PASS);
let last;
for (let i = 0; i < 12; i++) {
last = await call("PATCH", `/api/users/${ids.mia}`, {
cookie: boss,
body: { role: "analyst", currentPassword: "guess-" + i },
});
}
assert.equal(last.status, 429);
assert.equal(last.body.error.code, "ETOOMANY");
assert.equal(auth.findUser("mia").role, "member");
});
});
describe("in local mode", () => {
it("refuses every account change — there is no login to check", async () => {
delete process.env.AGENTHUB_MODE;
try {
const r = await call("POST", "/api/users", {
body: { username: "eve", password: "eve-pass-123", currentPassword: "x" },
});
assert.equal(r.status, 403);
assert.equal(r.body.error.code, "ELOCALMODE");
assert.equal(auth.findUser("eve"), undefined);
} finally {
process.env.AGENTHUB_MODE = "server";
}
});
});