import { sql } from 'drizzle-orm' import { Hono } from 'hono' import { setupSchema } from '../../shared/schemas.ts' import { startSession } from '../auth/middleware.ts' import { hashPassword } from '../auth/password.ts' import type { Db } from '../db/client.ts' import { organizations, users } from '../db/schema.ts' import { audit } from '../lib/audit.ts' import { validate, type AppEnv } from '../lib/http.ts' import { publicUser } from './users.ts' const SETUP_LOCK = 717_001 async function hasUsers(db: Db) { const rows = await db.select({ id: users.id }).from(users).limit(1) return rows.length > 0 } /** First-run setup: creates the organization and its first admin. Only works while there are no users. */ export const setupRoutes = new Hono() .get('/', async (c) => c.json({ needsSetup: !(await hasUsers(c.get('db'))) })) .post('/', validate('json', setupSchema), async (c) => { const input = c.req.valid('json') const passwordHash = await hashPassword(input.password) const user = await c.get('db').transaction(async (tx) => { await tx.execute(sql`select pg_advisory_xact_lock(${SETUP_LOCK})`) if (await hasUsers(tx)) return null const [organization] = await tx.insert(organizations).values({ name: input.organizationName }).returning() const [admin] = await tx .insert(users) .values({ organizationId: organization.id, email: input.email, name: input.name, role: 'admin', passwordHash }) .returning(publicUser) await audit(tx, { organizationId: organization.id, actorId: admin.id, entityType: 'organization', entityId: organization.id, action: 'setup' }) return admin }) if (!user) return c.json({ error: 'Setup has already been completed' }, 409) await startSession(c, user.id) return c.json({ user }, 201) })