import type { Handler } from 'hono'; import { getUserCount, createUser, replaceRoleGrants, USER_ROLES } from 'officerdb'; import { DEFAULT_ROLE_PERMISSIONS } from '@@/permissions/registry'; import argon2 from 'argon2'; import * as errors from '@@/custom-errors'; import { rememberUser } from '@@/_middlewares'; import { validatePassword } from './validate-password'; import { validateUsername } from './validate-username'; // Single-step bootstrap for the one account Officer supports: the server owner is created directly as // active, with no email-verification round-trip. Gated to an empty user table. export const bootstrapHandler: Handler = async function (ctx) { const body = ctx.get('body'); const userCount = await getUserCount(); if (userCount > 0) throw errors.FORBIDDEN('Registration is closed'); const email = body.email as string; const name = body.name as string; const username = body.username as string; const password = body.password as string; const confirmPassword = body.confirmPassword as string; if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { throw errors.BAD_REQUEST('Invalid email address'); } if (!name || !name.trim()) throw errors.BAD_REQUEST('Name is required'); const validUsername = validateUsername(username); validatePassword(password); if (password !== confirmPassword) throw errors.BAD_REQUEST('Passwords do not match'); const passwordHash = await argon2.hash(password); // The role is what makes this account the owner — isSuperAdmin and getOwnerUser both read it, and // nothing else confers it. Without this the first account would take the column's 'Member' default // and the platform would come up with no owner at all: no vault, no agent identity, and the web // origin locked to a Super Admin that does not exist. Bootstrap is gated on an empty user table // above, so this cannot promote anyone but the first account. const user = await createUser({ email, password: passwordHash, name: name.trim(), username: validUsername, status: 'Active', role: 'Super Admin', }); // Every other role starts with the baseline: terminal, chat and files at write. Done here because // bootstrap is the one moment that happens exactly once per install, so seeding cannot fight a later // revocation — take one of these away and nothing puts it back. // // Non-fatal. An owner who exists but whose roles hold nothing is a working server with a one-click fix; // failing bootstrap over it would leave a platform with no account at all. try { for (const role of USER_ROLES.filter((r) => r !== 'Super Admin')) { await replaceRoleGrants( role, // `capability` is the DATABASE's column name, renamed in the step that renames the table. DEFAULT_ROLE_PERMISSIONS.map((permission) => ({ capability: permission, level: 'write' as const })), ); } } catch (ex) { console.warn('[bootstrap] could not seed default role permissions', ex); } // The launch-time snapshot was taken while the user table was still empty. Without this the owner's // very first sign-in would be filed as an unknown identity. rememberUser(user); return ctx.json({ ok: true }); };