the owner can create accounts

POST /api/users plus an Add-account form in Settings > User management. Until now
createUser had one call site — bootstrap, gated on an empty user table — so every
non-owner account anywhere had been inserted into Postgres by hand.

Created accounts are Active. The column defaults to Unverified and signin refuses
anything else with a bare UNAUTHORIZED, which is exactly what made the hand-INSERT
route look like a wrong password.

Also closes a hole found while reading the write path: a second Super Admin was
storable. The CHECK constraint pins user 1's role but cannot see other rows, and
getOwnerUser() was LIMIT 1 with no ORDER BY, so two holders would have made "who owns
this server" a question the query plan answered — and that answer feeds the agent
sidecar's identity, vault access and origin scoping. Both write paths now refuse the
role and getOwnerUser() orders by id.

USER_DIRS and provisionUserDirs move into data-path.ts so the create handler and
scripts/provision-user-dirs.ts cannot disagree about what an account's skeleton is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:15:09 +00:00
co-authored by Claude Opus 5
parent b7184283e0
commit 69a31051ac
10 changed files with 408 additions and 29 deletions
+4 -20
View File
@@ -16,26 +16,10 @@
import { mkdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
// Mirrors the shape the owner's root grew organically. Most of these are also created on demand by
// whichever feature owns them (attachments, dashboards, email_accounts…), so pre-creating them buys
// legibility more than function — the tree shows what a user has without having to use it first.
//
// `home` is the exception, and the reason this exists at all: nothing creates it today. getOwnerHomeDir
// returns process.env.HOME_DIR whenever it is set, which it always is on a real install, so the
// per-user home has never actually been reached. It is where a non-owner's sessions will run.
const USER_DIRS = [
'home',
'attachments',
'cache',
'dashboards',
'email_accounts',
'general_chat_sessions',
'logs',
'sidecar',
] as const;
// The list and DATA_PATH itself come from the platform rather than being restated here. The owner's
// create-account handler provisions the same skeleton, and a script that drifted from it would produce
// accounts that differ by how they were made. Importing data-path.ts pulls in no database and no server.
import { DATA_PATH, USER_DIRS } from '../src/servers/data-path';
const DRY_RUN = process.env.DRY_RUN === '1';
const emails = process.argv.slice(2).filter(Boolean);