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:
@@ -42,6 +42,37 @@ export const getHomeDir = (email: string) => join(DATA_PATH, email, 'home');
|
||||
// terminals/chats/tasks share config and credentials with the shell they use outside Officer.
|
||||
export const getOwnerHomeDir = (email: string): string => process.env.HOME_DIR ?? getHomeDir(email);
|
||||
|
||||
// The directory skeleton a new account gets under DATA_PATH.
|
||||
//
|
||||
// Most of these are also created on demand by whichever feature owns them, so pre-creating them buys
|
||||
// legibility more than function — the tree shows what an account has without it having to be used first.
|
||||
// `home` is the exception and the reason this exists: nothing else creates it, and it is where a
|
||||
// non-owner's sessions would run.
|
||||
//
|
||||
// Single-sourced here rather than in the script that used to own the list, because there are now two
|
||||
// callers — `scripts/provision-user-dirs.ts` and the owner's create-account handler — and a skeleton
|
||||
// that differs depending on how the account was made is a bug nobody would think to look for.
|
||||
export const USER_DIRS = [
|
||||
'home',
|
||||
'attachments',
|
||||
'cache',
|
||||
'dashboards',
|
||||
'email_accounts',
|
||||
'general_chat_sessions',
|
||||
'logs',
|
||||
'sidecar',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Create an account's root and its skeleton. Idempotent — an existing directory is left exactly as it is.
|
||||
*
|
||||
* Keyed on email because that is what the on-disk layout uses everywhere else (`DATA_PATH/<email>/…`).
|
||||
* Renaming an account's email would orphan its directory; that is pre-existing and not this function's
|
||||
* problem, but it is the reason nothing here derives a path from the id.
|
||||
*/
|
||||
export const provisionUserDirs = (email: string): void => {
|
||||
for (const dir of USER_DIRS) mkdirSync(join(DATA_PATH, email, dir), { recursive: true });
|
||||
};
|
||||
|
||||
export const getTmpAttachmentsDir = (email: string) => join(DATA_PATH, email, 'attachments', 'tmp');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user