import { randomUUID } from "node:crypto"; import type { FastifyInstance } from "fastify"; import { eq, laneDevices, setupState, type Db } from "@parking/db"; import { registerBuiltinDrivers, registry, setDeviceLogSink, type DeviceCategory, } from "@parking/devices"; import { requireRole } from "../auth.js"; // First-run setup API. The admin reads the driver catalog and assigns devices // per lane. See wiki/concepts/first-run-setup.md. interface AssignBody { lane: number; category: DeviceCategory; driverId: string; config: Record; } export async function setupRoutes(app: FastifyInstance, db: Db): Promise { registerBuiltinDrivers(); setDeviceLogSink((line) => app.log.info(line)); // Catalog of selectable drivers per category (no secrets — schema only). app.get("/api/setup/catalog", async () => registry.catalog()); // Current setup status + assignments. app.get( "/api/setup/state", { preHandler: requireRole("admin") }, async () => { const state = await db.select().from(setupState).where(eq(setupState.id, 1)).get(); const assignments = await db.select().from(laneDevices).all(); return { completedAt: state?.completedAt ?? null, assignments }; }, ); // Assign a device to a lane. Validates the chosen driver + config against the // registry before persisting; rejects unknown drivers / missing config. app.post<{ Body: AssignBody }>( "/api/setup/assign", { preHandler: requireRole("admin") }, async (req, reply) => { const { lane, category, driverId, config } = req.body; const driver = registry.get(driverId); if (!driver || driver.category !== category) { return reply.code(400).send({ error: `invalid driver for ${category}: ${driverId}` }); } try { registry.create(driverId, config); // validates required fields } catch (err) { return reply.code(400).send({ error: (err as Error).message }); } const row = { id: randomUUID(), lane, category, driverId, config, enabled: true, }; await db.insert(laneDevices).values(row); return reply.code(201).send(row); }, ); // Mark first-run setup complete. app.post( "/api/setup/complete", { preHandler: requireRole("admin") }, async () => { const completedAt = new Date().toISOString(); await db .insert(setupState) .values({ id: 1, completedAt }) .onConflictDoUpdate({ target: setupState.id, set: { completedAt } }); return { completedAt }; }, ); }