provisioning: drop multi-user remnants, provision the super admin on bootstrap
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/<email> was never provisioned up front. Now it is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/<email> + 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 });
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
// 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<void> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user