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
View File
@@ -5,6 +5,7 @@ import { isSuperAdmin } from '@@/super-admin';
import * as errors from '@@/custom-errors';
import { updateUserHandler } from './update-user';
import { listUsersHandler, updateUserRoleHandler, deleteUserHandler } from './manage-users';
import { createUserHandler } from './create-user';
import { capabilityAdminRouter } from './capabilities-routes';
export const usersRouter = createRouter();
@@ -24,6 +25,9 @@ const ownerGate: MiddlewareHandler = async (ctx, next) => {
};
usersRouter.get('/', ownerGate, listUsersHandler);
// POST, not PUT — and worth noting they sit one line apart. `PUT /` is the selfService exception every
// account may call on itself; `POST /` creates somebody else and is the owner's alone.
usersRouter.post('/', ownerGate, createUserHandler);
usersRouter.patch('/:id/role', ownerGate, updateUserRoleHandler);
usersRouter.delete('/:id', ownerGate, deleteUserHandler);