feat: v2.7.6 — edit users in one form, Settings → Account, display names, copy over plain HTTP
- Users page: one Edit user form (display name, email, role, active, a new or generated password, workspaces given or taken back, sign out everywhere), validated as a whole before anything is written (PATCH /api/users/:id, GET /api/users/:id); users.display_name/email added migration-safe. - Settings → Account for every signed-in user: profile, own password (the current one required, this login kept), sign out other devices (/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie only, never a pane token). - Display names wherever a person is named (sidebar, workspace owner groups, Users page and audit log); owner_display_name / reader_display additive. - Users page: admins add and remove Claude/Codex pool accounts. - Folder browser: an admin with their own Linux account opens in their home; the "Server folder" tick opens the host's (never remembered). - AI Proxy: "Config 9router" beside Add profile when 9router is installed, opened on the dashboard's own host (GET /api/ai-proxies/9router). - Terminal: a tmux selection reaches the clipboard over plain HTTP too (writeClipboard's textarea fallback, focus handed back). - Chat: a file the session touched outside the workspace previews (GET /api/sessions/:id/file). - install-system: `!requiretty` only when visudo accepts it (sudo-rs on Ubuntu 25.10+ rejects the setting). - openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,14 @@
|
||||
* - POST /api/auth/login (Auth)
|
||||
* - POST /api/auth/logout (Auth)
|
||||
* - GET /api/accounts (Auth) — the Claude and Codex account pools, no keys or tokens
|
||||
* - POST /api/accounts, DELETE /api/accounts/{name} (Auth) — an admin adds/removes one
|
||||
* - GET /api/users (Auth) — every account's usage (admin/analyst)
|
||||
* - GET /api/users/audit (Auth) — staff reads of developers' sessions
|
||||
* - GET/PUT /api/me/prefs, POST /api/me/onboarded (Auth) — the account's own UI preferences
|
||||
* - GET/PUT /api/me/instructions (Auth) — the account's own agent instructions
|
||||
* - GET/PATCH /api/me/account, POST /api/me/password, POST /api/me/sign-out-others
|
||||
* (Auth) — Settings → Account: the account's own profile, password and logins
|
||||
* - GET/PATCH /api/users/{id} (Auth) — the Users page's Edit user form
|
||||
* 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>
|
||||
@@ -38,6 +42,21 @@ const schemas = {
|
||||
username: { type: "string" },
|
||||
role: { type: "string", enum: ["admin", "analyst", "developer"] },
|
||||
disabled: { type: "boolean" },
|
||||
displayName: { type: "string", nullable: true },
|
||||
email: { type: "string", nullable: true },
|
||||
createdAt: { type: "string", nullable: true },
|
||||
},
|
||||
},
|
||||
MyAccount: {
|
||||
type: "object",
|
||||
required: ["mode"],
|
||||
properties: {
|
||||
mode: { type: "string", enum: ["local", "server"] },
|
||||
user: { $ref: "#/components/schemas/AuthUser" },
|
||||
workspaces: { type: "integer" },
|
||||
sessions: { type: "integer" },
|
||||
logins: { type: "integer", description: "Unexpired logins of the account" },
|
||||
lastLoginAt: { type: "string", nullable: true },
|
||||
},
|
||||
},
|
||||
AuthMe: {
|
||||
@@ -88,6 +107,9 @@ const changeResponses = {
|
||||
429: err("ETOOMANY — 10 wrong passwords in 15 minutes"),
|
||||
};
|
||||
|
||||
const accountBody = {
|
||||
"application/json": { schema: { $ref: "#/components/schemas/MyAccount" } },
|
||||
};
|
||||
const prefsBody = {
|
||||
"application/json": { schema: { $ref: "#/components/schemas/UserPrefs" } },
|
||||
};
|
||||
@@ -182,6 +204,85 @@ const paths = {
|
||||
responses: { 200: { description: "Marked", content: prefsBody }, 401: err("EAUTH") },
|
||||
},
|
||||
},
|
||||
"/api/me/account": {
|
||||
get: {
|
||||
tags: ["Auth"],
|
||||
operationId: "getMyAccount",
|
||||
summary: 'Settings → Account: the signed-in account (local mode: {mode: "local"})',
|
||||
responses: {
|
||||
200: { description: "Account", content: accountBody },
|
||||
401: err("EAUTH"),
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["Auth"],
|
||||
operationId: "updateMyAccount",
|
||||
summary:
|
||||
'The account\'s own display name and email (""/null clears). Login cookie only, audited',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
displayName: { type: "string", nullable: true, maxLength: 100 },
|
||||
email: { type: "string", nullable: true, maxLength: 254 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: { description: "Updated", content: accountBody },
|
||||
400: err("EBADFIELD / EBADEMAIL"),
|
||||
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
||||
403: err("ELOCALMODE"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/me/password": {
|
||||
post: {
|
||||
tags: ["Auth"],
|
||||
operationId: "changeMyPassword",
|
||||
summary:
|
||||
"Change the account's own password: the current one required (throttled); this login stays, every other ends",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["currentPassword", "newPassword"],
|
||||
properties: {
|
||||
currentPassword: { type: "string" },
|
||||
newPassword: { type: "string", minLength: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: { description: "Changed ({ok: true})" },
|
||||
400: err("EBADPASSWORD"),
|
||||
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
||||
403: err("EREAUTH (current password wrong) / ELOCALMODE"),
|
||||
429: err("ETOOMANY — 10 wrong passwords in 15 minutes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/me/sign-out-others": {
|
||||
post: {
|
||||
tags: ["Auth"],
|
||||
operationId: "signOutMyOtherLogins",
|
||||
summary: "End every login of the account but this one; answers {ended}",
|
||||
responses: {
|
||||
200: { description: "Ended" },
|
||||
401: err("EAUTH — no login cookie (a pane token is refused)"),
|
||||
403: err("ELOCALMODE"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/users": {
|
||||
get: {
|
||||
tags: ["Auth"],
|
||||
@@ -263,17 +364,38 @@ const paths = {
|
||||
},
|
||||
},
|
||||
"/api/users/{id}": {
|
||||
get: {
|
||||
tags: ["Auth"],
|
||||
operationId: "getServerUser",
|
||||
summary: "One account with its logins and the workspaces it owns (admin/analyst)",
|
||||
parameters: [idParam],
|
||||
responses: {
|
||||
200: { description: "Account" },
|
||||
403: err("EADMIN — a developer"),
|
||||
404: err("ENOUSER"),
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
tags: ["Auth"],
|
||||
operationId: "updateServerUser",
|
||||
summary:
|
||||
"Admin: change a role and/or disable (ending every login) or enable. Never your own (ESELF)",
|
||||
"Admin (Edit user): display name, email, role, disabled, a new password, workspaces given or taken back, sign out everywhere — every field validated before any is written. Never your own role or disable (ESELF)",
|
||||
parameters: [idParam],
|
||||
requestBody: changeBody({
|
||||
displayName: { type: "string", nullable: true, maxLength: 100 },
|
||||
email: { type: "string", nullable: true, maxLength: 254 },
|
||||
role: { type: "string", enum: ["admin", "analyst", "developer"] },
|
||||
disabled: { type: "boolean" },
|
||||
password: { type: "string", minLength: 1 },
|
||||
addWorkspaces: { type: "array", items: { type: "integer" } },
|
||||
removeWorkspaces: { type: "array", items: { type: "integer" } },
|
||||
signOut: { type: "boolean", enum: [true] },
|
||||
}),
|
||||
responses: changeResponses,
|
||||
responses: {
|
||||
...changeResponses,
|
||||
400: err("EBADPASSWORD / EBADROLE / EBADFIELD / EBADEMAIL"),
|
||||
409: err("ESELF / ENOTOWNED / EOWNERFOLDER"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/users/{id}/password": {
|
||||
@@ -322,6 +444,8 @@ const paths = {
|
||||
session_id: { type: "string", nullable: true },
|
||||
reader: { type: "string", nullable: true },
|
||||
owner: { type: "string", nullable: true },
|
||||
reader_display: { type: "string", nullable: true },
|
||||
owner_display: { type: "string", nullable: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -401,6 +525,89 @@ const paths = {
|
||||
},
|
||||
},
|
||||
},
|
||||
post: {
|
||||
tags: ["Auth"],
|
||||
operationId: "addPoolAccount",
|
||||
summary:
|
||||
"Admin: add a Claude (saved login, pasted credentials or setup-token) or Codex (API key / auth.json text) account to the pool, with their own password again. The secret is never answered back",
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["tool", "currentPassword"],
|
||||
properties: {
|
||||
tool: { type: "string", enum: ["claude", "codex"] },
|
||||
name: { type: "string", description: "Defaults to the profile's name" },
|
||||
profile: {
|
||||
type: "string",
|
||||
description: "Claude: a saved switch-user profile, moved into the pool",
|
||||
},
|
||||
credentials: {
|
||||
type: "string",
|
||||
description: "Claude: the text of a .credentials.json",
|
||||
},
|
||||
token: {
|
||||
type: "string",
|
||||
description: "Claude: a setup-token; Codex: an API key or auth.json text",
|
||||
},
|
||||
currentPassword: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: { description: "`{account: {name, addedAt, …}}` — no token" },
|
||||
400: err("EBADTOOL, EBADNAME, EBADTOKEN"),
|
||||
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
|
||||
409: err(
|
||||
"EEXISTS — the name or account is taken; EACTIVE — the host's CLI uses that profile"
|
||||
),
|
||||
502: err("EUPSTREAM — Anthropic could not be reached"),
|
||||
429: err("ETOOMANY — too many wrong passwords"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/accounts/profiles": {
|
||||
get: {
|
||||
tags: ["Auth"],
|
||||
operationId: "listPoolProfiles",
|
||||
summary: "Saved switch-user profiles an admin may move into the Claude pool (no token)",
|
||||
responses: {
|
||||
200: { description: "`{profiles: [{name, label, email, subscriptionType, active}]}`" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/accounts/{name}": {
|
||||
delete: {
|
||||
tags: ["Auth"],
|
||||
operationId: "removePoolAccount",
|
||||
summary: "Admin: remove an account from the pool, with their own password again",
|
||||
parameters: [{ name: "name", in: "path", required: true, schema: { type: "string" } }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["tool", "currentPassword"],
|
||||
properties: {
|
||||
tool: { type: "string", enum: ["claude", "codex"] },
|
||||
currentPassword: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: { description: "`{ok: true}`" },
|
||||
403: err("EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE"),
|
||||
404: err("ENOACCOUNT"),
|
||||
429: err("ETOOMANY — too many wrong passwords"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/auth/me": {
|
||||
get: {
|
||||
|
||||
Reference in New Issue
Block a user