From d604b1e72219f0d4c37ed39c352cdebbd8fd6732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 25 Jul 2026 15:32:53 +0000 Subject: [PATCH] provisioning: drop multi-user remnants, provision the super admin on bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the multi-user provisioning leftovers (the provision-existing-users.sh migration was already deleted in the prior commit): - gut provisionVncEnv from provision.ts (per-user startxfce4 virtual desktop, dead since the switch to mirroring :0 — vnc-manager.ts self-provisions its own passwd) - drop the Pi `.pi/agent/sessions` seed and the now-orphaned `run` helper Wire provisioning into bootstrapHandler: the super admin (first user) is created via createUser, which never called provisionUserEnvironment — only the invite/verify flow did. So the single user's DATA_PATH/ was never provisioned up front. Now it is. Co-Authored-By: Claude Opus 4.8 --- TODO.md | 10 +++--- src/servers/api/auth/bootstrap.ts | 10 +++++- src/servers/api/auth/verify.ts | 2 +- src/servers/api/users/provision.ts | 58 ------------------------------ 4 files changed, 15 insertions(+), 65 deletions(-) diff --git a/TODO.md b/TODO.md index 28d02481..783444ad 100644 --- a/TODO.md +++ b/TODO.md @@ -14,11 +14,11 @@ Deferred work. Context: Officer is collapsing from multi-tenant / open-source-re `channels/send-claude-code.ts`. After this, `toShellUsername` has one caller left (`provision.ts` → `generateClaudeSettings`) and may be inlinable. -- [ ] **Delete or gut `scripts/provision-existing-users.sh`.** Line ~100 runs - `sudo useradd -d "$HOME_DIR" -s /bin/zsh -M "$shell_user"`. This is what created the four - vestigial Unix accounts (`andrepadez`, `john-wick`, `fedra`, `miguelbenoliel`) that cluttered - the lightdm greeter. Nothing in the server creates Unix users; if this script runs again they - come back. +- [x] **Delete or gut `scripts/provision-existing-users.sh`.** Done — deleted the script (it ran + `sudo useradd …`, the source of the vestigial `andrepadez`/`john-wick`/`fedra`/`miguelbenoliel` + Unix accounts). Also dropped the dead per-user VNC desktop provisioning (`provisionVncEnv`, the + `startxfce4` xstartup) from `provision.ts` — the mirror self-provisions its passwd in + `vnc-manager.ts` — and the Pi `.pi/agent/sessions` seed. - [ ] **Collapse the rest of the multi-tenant machinery.** Candidates, in rough order of payoff: roles (`Super Admin`/`Member`), the sandboxed-vs-unsandboxed path split, per-email home dirs diff --git a/src/servers/api/auth/bootstrap.ts b/src/servers/api/auth/bootstrap.ts index f6a566eb..50c1aae2 100644 --- a/src/servers/api/auth/bootstrap.ts +++ b/src/servers/api/auth/bootstrap.ts @@ -6,6 +6,7 @@ import argon2 from 'argon2'; import * as errors from '@@/custom-errors'; import { validatePassword } from './validate-password'; import { validateUsername } from './validate-username'; +import { provisionUserEnvironment } from '../users/provision'; export const bootstrapHandler: Handler = async function (ctx) { const body = ctx.get('body'); @@ -49,7 +50,7 @@ export const bootstrapHandler: Handler = async function (ctx) { const passwordHash = await argon2.hash(password); - await createUser({ + const user = await createUser({ email: payload.email, password: passwordHash, name: name.trim(), @@ -58,5 +59,12 @@ export const bootstrapHandler: Handler = async function (ctx) { status: 'Active', }); + // Provision the super admin's environment (DATA_PATH/ + configs) on creation. This is the only + // account-creation flow for a single-user platform, and — unlike the invite/verify flow — nothing + // else runs provisioning for the first user. Fire-and-forget, mirroring verifyHandler. + provisionUserEnvironment(user.email, user.username ?? validUsername).catch((err) => { + console.error('[bootstrap] failed to provision super admin environment:', err); + }); + return ctx.json({ ok: true }); }; diff --git a/src/servers/api/auth/verify.ts b/src/servers/api/auth/verify.ts index 0306d140..7b3893a2 100644 --- a/src/servers/api/auth/verify.ts +++ b/src/servers/api/auth/verify.ts @@ -43,7 +43,7 @@ export const verifyHandler: Handler = async function (ctx) { const finalUser = await getUserById(userInfo.id); if (!finalUser) throw errors.NOT_FOUND('User not found'); - // Provision user environment (directories, configs, VNC) + // Provision user environment (directories, configs) provisionUserEnvironment(finalUser.email, finalUser.username ?? '').catch((err) => { console.error('[verify] failed to provision user environment:', err); }); diff --git a/src/servers/api/users/provision.ts b/src/servers/api/users/provision.ts index 08ce239f..5a4cde19 100644 --- a/src/servers/api/users/provision.ts +++ b/src/servers/api/users/provision.ts @@ -5,14 +5,6 @@ import { generateContainerContext, generateClaudeSettings } from '@@/generate-co const TEMPLATE_DIR = join(import.meta.dir, '../terminal/templates'); -const run = (cmd: string[], opts?: { cwd?: string }): boolean => { - const result = Bun.spawnSync({ cmd, stdout: 'ignore', stderr: 'pipe', ...opts }); - if (result.exitCode !== 0) { - console.error(`[provision] command failed: ${cmd.join(' ')}`, result.stderr.toString().trim()); - } - return result.exitCode === 0; -}; - const copyTemplate = async (src: string, dest: string) => { if (existsSync(dest)) return; const content = await Bun.file(src).text(); @@ -45,9 +37,6 @@ export async function provisionUserEnvironment(email: string, username: string): const settingsContent = await Bun.file(settingsFile).text(); await Bun.write(join(claudeDir, 'settings.json'), settingsContent); - // Provision VNC environment - await provisionVncEnv(homeDir); - console.log(`[provision] provisioning complete for ${email}`); return true; } @@ -93,53 +82,6 @@ async function seedShellConfigs(homeDir: string): Promise { // Ensure .local/bin exists mkdirSync(join(homeDir, '.local', 'bin'), { recursive: true }); - - // Ensure .pi/agent/sessions exists - mkdirSync(join(homeDir, '.pi', 'agent', 'sessions'), { recursive: true }); -} - -async function provisionVncEnv(homeDir: string): Promise { - const vncDir = join(homeDir, '.vnc'); - mkdirSync(vncDir, { recursive: true }); - - // Skip if already provisioned - if (existsSync(join(vncDir, 'passwd'))) return; - - // Generate random 8-char password - const password = Array.from(crypto.getRandomValues(new Uint8Array(6))) - .map((b) => String.fromCharCode(33 + (b % 94))) - .join(''); - - // Write plaintext password (for API to read) - await Bun.write(join(vncDir, 'password'), password); - - // Create encrypted passwd using vncpasswd -f - const proc = Bun.spawnSync({ - cmd: ['bash', '-c', `echo '${password.replace(/'/g, "'\\''")}' | vncpasswd -f`], - stdout: 'pipe', - stderr: 'ignore', - }); - - if (proc.exitCode === 0 && proc.stdout.byteLength > 0) { - await Bun.write(join(vncDir, 'passwd'), proc.stdout); - } - - // Write xstartup - const xstartup = `#!/bin/sh -unset SESSION_MANAGER -unset DBUS_SESSION_BUS_ADDRESS -eval $(dbus-launch --sh-syntax) -export DBUS_SESSION_BUS_ADDRESS -exec startxfce4 -`; - await Bun.write(join(vncDir, 'xstartup'), xstartup); - - // Set permissions - run(['chmod', '+x', join(vncDir, 'xstartup')]); - run(['chmod', '600', join(vncDir, 'passwd')]); - run(['chmod', '600', join(vncDir, 'password')]); - - console.log(`[provision] VNC environment provisioned at ${vncDir}`); } export function deprovisionUserEnvironment(email: string, _username: string): boolean {