import { Body, Controller, ForbiddenException, Get, Inject, InternalServerErrorException, Post, } from '@nestjs/common'; import { randomBytes, createHash } from 'node:crypto'; import { count, eq, type Db, users as usersTable, adminTokens } from '@mosaic/db'; import type { Auth } from '@mosaic/auth'; import { v4 as uuid } from 'uuid'; import { AUTH } from '../auth/auth.tokens.js'; import { DB } from '../database/database.module.js'; import type { BootstrapSetupDto, BootstrapStatusDto, BootstrapResultDto } from './bootstrap.dto.js'; @Controller('api/bootstrap') export class BootstrapController { constructor( @Inject(AUTH) private readonly auth: Auth, @Inject(DB) private readonly db: Db, ) {} @Get('status') async status(): Promise { const [result] = await this.db.select({ total: count() }).from(usersTable); return { needsSetup: (result?.total ?? 0) === 0 }; } @Post('setup') async setup(@Body() dto: BootstrapSetupDto): Promise { // Only allow setup when zero users exist const [result] = await this.db.select({ total: count() }).from(usersTable); if ((result?.total ?? 0) > 0) { throw new ForbiddenException('Setup already completed — users exist'); } // Create admin user via BetterAuth API const authApi = this.auth.api as unknown as { createUser: (opts: { body: { name: string; email: string; password: string; role?: string }; }) => Promise<{ user: { id: string; name: string; email: string }; }>; }; const created = await authApi.createUser({ body: { name: dto.name, email: dto.email, password: dto.password, role: 'admin', }, }); // Verify user was created const [user] = await this.db .select() .from(usersTable) .where(eq(usersTable.id, created.user.id)) .limit(1); if (!user) throw new InternalServerErrorException('User created but not found'); // Ensure role is admin (createUser may not set it via BetterAuth) if (user.role !== 'admin') { await this.db.update(usersTable).set({ role: 'admin' }).where(eq(usersTable.id, user.id)); } // Generate admin API token const plaintext = randomBytes(32).toString('hex'); const tokenHash = createHash('sha256').update(plaintext).digest('hex'); const tokenId = uuid(); const [token] = await this.db .insert(adminTokens) .values({ id: tokenId, userId: user.id, tokenHash, label: 'Initial setup token', scope: 'admin', }) .returning(); return { user: { id: user.id, name: user.name, email: user.email, role: 'admin', }, token: { id: token!.id, plaintext, label: token!.label, }, }; } }