Files
AgentHub/server/openapi-extra/auth.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

347 lines
11 KiB
JavaScript

/**
* @file OpenAPI 3.0 fragment for server mode's login routes (server/routes/auth.js):
* - GET /api/auth/me (Auth)
* - POST /api/auth/login (Auth)
* - POST /api/auth/logout (Auth)
* - GET /api/accounts (Auth) — the Claude account pool, no tokens
* - GET /api/users (Auth) — every account's usage (admin/analyst)
* - GET /api/users/audit (Auth) — staff reads of members' sessions
* Exports `{ tags, schemas, paths }`, combined into the spec by
* server/openapi-extra.js. See docs/SERVER-MODE.md.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const tags = [
{
name: "Auth",
description:
'Server mode\'s login (AGENTHUB_MODE=server). In local mode — the default — only GET /api/auth/me answers, with {mode: "local"}.',
},
];
const schemas = {
AuthUser: {
type: "object",
required: ["id", "username", "role"],
properties: {
id: { type: "integer" },
username: { type: "string" },
role: { type: "string", enum: ["admin", "analyst", "member"] },
disabled: { type: "boolean" },
},
},
AuthMe: {
type: "object",
required: ["mode"],
properties: {
mode: { type: "string", enum: ["local", "server"] },
user: {
description: "Server mode only: the logged-in user, or null.",
nullable: true,
allOf: [{ $ref: "#/components/schemas/AuthUser" }],
},
},
},
};
const err = (description) => ({
description,
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } },
});
// The Users page's account changes: every body carries the admin's own
// password again (`currentPassword`).
const userBody = {
"application/json": {
schema: { type: "object", properties: { user: { $ref: "#/components/schemas/AuthUser" } } },
},
};
const idParam = { name: "id", in: "path", required: true, schema: { type: "integer" } };
const changeBody = (properties, required = []) => ({
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: [...required, "currentPassword"],
properties: { ...properties, currentPassword: { type: "string" } },
},
},
},
});
const changeResponses = {
200: { description: "Changed", content: userBody },
400: err("EBADPASSWORD / EBADROLE / EBADFIELD"),
403: err("EREAUTH (own password wrong) / EREADONLY / EADMIN / ELOCALMODE"),
404: err("ENOUSER / ENOLANE"),
409: err("ESELF — your own role or disable"),
429: err("ETOOMANY — 10 wrong passwords in 15 minutes"),
};
const paths = {
"/api/users": {
get: {
tags: ["Auth"],
operationId: "listServerUsers",
summary:
"Every account with its workspaces, sessions and 7/30-day usage (admin/analyst; 403 EADMIN for a member)",
responses: {
200: {
description: "Accounts",
content: {
"application/json": {
schema: {
type: "object",
properties: {
users: {
type: "array",
items: {
allOf: [
{ $ref: "#/components/schemas/AuthUser" },
{
type: "object",
properties: {
workspaces: { type: "integer" },
sessions: { type: "integer" },
last7d: {
type: "object",
properties: { cost: { type: "number" }, tokens: { type: "integer" } },
},
last30d: {
type: "object",
properties: { cost: { type: "number" }, tokens: { type: "integer" } },
},
},
},
],
},
},
},
},
},
},
},
403: err("EADMIN — a member"),
},
},
post: {
tags: ["Auth"],
operationId: "createServerUser",
summary:
"Admin: add an account. Needs the admin's own password again (currentPassword); audited",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["username", "password", "currentPassword"],
properties: {
username: { type: "string" },
password: { type: "string", minLength: 8 },
role: { type: "string", enum: ["admin", "analyst", "member"], default: "member" },
currentPassword: { type: "string" },
},
},
},
},
},
responses: {
201: { description: "Created", content: userBody },
400: err("EBADUSER / EBADPASSWORD / EBADROLE"),
403: err("EREAUTH (own password wrong) / EREADONLY / EADMIN / ELOCALMODE"),
409: err("EEXISTS"),
429: err("ETOOMANY — 10 wrong passwords in 15 minutes"),
},
},
},
"/api/users/{id}": {
patch: {
tags: ["Auth"],
operationId: "updateServerUser",
summary:
"Admin: change a role and/or disable (ending every login) or enable. Never your own (ESELF)",
parameters: [idParam],
requestBody: changeBody({
role: { type: "string", enum: ["admin", "analyst", "member"] },
disabled: { type: "boolean" },
}),
responses: changeResponses,
},
},
"/api/users/{id}/password": {
post: {
tags: ["Auth"],
operationId: "resetServerUserPassword",
summary: "Admin: a new password for that user; every login of theirs ends",
parameters: [idParam],
requestBody: changeBody({ password: { type: "string", minLength: 8 } }, ["password"]),
responses: changeResponses,
},
},
"/api/users/{id}/workspaces": {
post: {
tags: ["Auth"],
operationId: "assignServerUserWorkspace",
summary: "Admin: make that user the owner of a workspace (lane)",
parameters: [idParam],
requestBody: changeBody({ laneId: { type: "integer" } }, ["laneId"]),
responses: changeResponses,
},
},
"/api/users/audit": {
get: {
tags: ["Auth"],
operationId: "listAuditLog",
summary: "Admin/analyst reads of sessions members own, newest first",
parameters: [{ name: "limit", in: "query", schema: { type: "integer", maximum: 1000 } }],
responses: {
200: {
description: "Entries",
content: {
"application/json": {
schema: {
type: "object",
properties: {
entries: {
type: "array",
items: {
type: "object",
properties: {
id: { type: "integer" },
at: { type: "string" },
method: { type: "string" },
path: { type: "string" },
session_id: { type: "string", nullable: true },
reader: { type: "string", nullable: true },
owner: { type: "string", nullable: true },
},
},
},
},
},
},
},
},
403: err("EADMIN — a member"),
},
},
},
"/api/accounts": {
get: {
tags: ["Auth"],
operationId: "listPoolAccounts",
summary: "The Claude account pool as the balancer sees it (never a token)",
parameters: [
{
name: "force",
in: "query",
schema: { type: "string", enum: ["1"] },
description: "Re-read every account's quota first",
},
],
responses: {
200: {
description: "Accounts with live panes, recent spend and quota",
content: {
"application/json": {
schema: {
type: "object",
properties: {
blockPercent: { type: "number" },
accounts: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
addedAt: { type: "string" },
expiresAt: { type: "string" },
expired: { type: "boolean" },
livePanes: { type: "integer" },
spend5h: { type: "number" },
spend7d: { type: "number" },
quota: { type: "object", nullable: true },
quotaPercent: { type: "number", nullable: true },
full: { type: "boolean" },
},
},
},
},
},
},
},
},
},
},
},
"/api/auth/me": {
get: {
tags: ["Auth"],
operationId: "authMe",
summary: "The dashboard's mode and, in server mode, who is logged in",
responses: {
200: {
description: "Mode and user",
content: { "application/json": { schema: { $ref: "#/components/schemas/AuthMe" } } },
},
},
},
},
"/api/auth/login": {
post: {
tags: ["Auth"],
operationId: "authLogin",
summary: "Log in; sets the httpOnly, SameSite=Strict cookie agenthub_sid",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["username", "password"],
properties: { username: { type: "string" }, password: { type: "string" } },
},
},
},
},
responses: {
200: {
description: "Logged in",
content: {
"application/json": {
schema: {
type: "object",
properties: { user: { $ref: "#/components/schemas/AuthUser" } },
},
},
},
},
401: err("EBADLOGIN — wrong username or password, or a disabled user"),
403: err("EBADORIGIN — a cross-origin request"),
404: err("ENOTSERVER — the dashboard is in local mode"),
429: err("ETOOMANY — too many failed logins from this address (15 minutes)"),
},
},
},
"/api/auth/logout": {
post: {
tags: ["Auth"],
operationId: "authLogout",
summary: "End this login and clear its cookie",
responses: {
200: {
description: "Logged out",
content: {
"application/json": {
schema: { type: "object", properties: { ok: { type: "boolean" } } },
},
},
},
},
},
},
};
module.exports = { tags, schemas, paths };