import type { FastifyInstance } from 'fastify'; import { spawn } from 'node:child_process'; import { z } from 'zod'; import { adminSettings, auditLog, buildLogs, builds, count, createDb, desc, eq, gte, inArray, mcpServers, memberships, organizations, sql, toolCallMetrics, users, } from '@bmm/db'; import { SYSTEM_PROMPT } from '@bmm/llm'; import { requireAdmin } from '../plugins/session.js'; import { getRedis } from '../lib/redis.js'; import { getBuildQueue } from '../lib/queue.js'; import { audit } from '../lib/audit.js'; const db = createDb(); const PROMPT_KEY = 'system_prompt_override'; export async function adminRoutes(app: FastifyInstance): Promise { // ---- Overview metrics ---- app.get('/v1/admin/overview', { preHandler: requireAdmin }, async (_req, reply) => { const dayAgo = new Date(Date.now() - 24 * 3600_000); const sevenDaysAgo = new Date(Date.now() - 7 * 86400_000); const [ userCount, orgCount, serverCount, buildCount, callCount, liveCount, failedBuildCount, newUsersLast7d, newServersLast7d, ] = await Promise.all([ db.select({ c: count() }).from(users).then((r) => Number(r[0]?.c ?? 0)), db.select({ c: count() }).from(organizations).then((r) => Number(r[0]?.c ?? 0)), db.select({ c: count() }).from(mcpServers).then((r) => Number(r[0]?.c ?? 0)), db.select({ c: count() }).from(builds).then((r) => Number(r[0]?.c ?? 0)), db.select({ c: count() }).from(toolCallMetrics).then((r) => Number(r[0]?.c ?? 0)), db .select({ c: count() }) .from(mcpServers) .where(eq(mcpServers.status, 'live')) .then((r) => Number(r[0]?.c ?? 0)), db .select({ c: count() }) .from(builds) .where(eq(builds.status, 'failed')) .then((r) => Number(r[0]?.c ?? 0)), db .select({ c: count() }) .from(users) .where(gte(users.createdAt, sevenDaysAgo)) .then((r) => Number(r[0]?.c ?? 0)), db .select({ c: count() }) .from(mcpServers) .where(gte(mcpServers.createdAt, sevenDaysAgo)) .then((r) => Number(r[0]?.c ?? 0)), ]); // Server status breakdown const statusBreakdown = await db .select({ status: mcpServers.status, c: count() }) .from(mcpServers) .groupBy(mcpServers.status); // Recent activity from audit log const recent = await db .select() .from(auditLog) .orderBy(desc(auditLog.createdAt)) .limit(15); // Builds in last 24h with status const recentBuilds = await db .select({ status: builds.status, c: count() }) .from(builds) .where(gte(builds.createdAt, dayAgo)) .groupBy(builds.status); return reply.send({ totals: { users: userCount, orgs: orgCount, servers: serverCount, liveServers: liveCount, builds: buildCount, failedBuilds: failedBuildCount, toolCalls: callCount, }, trends: { newUsersLast7d, newServersLast7d, }, statusBreakdown, recentBuilds24h: recentBuilds, recentActivity: recent, }); }); // ---- Users ---- app.get('/v1/admin/users', { preHandler: requireAdmin }, async (req, reply) => { const Query = z.object({ search: z.string().optional(), limit: z.coerce.number().min(1).max(500).default(100), }); const parsed = Query.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'invalid_query' }); const rows = await db.select().from(users).orderBy(desc(users.createdAt)).limit(parsed.data.limit); const filtered = parsed.data.search ? rows.filter((u) => u.email.toLowerCase().includes(parsed.data.search!.toLowerCase()) || (u.name?.toLowerCase().includes(parsed.data.search!.toLowerCase()) ?? false), ) : rows; // attach org + server count const enriched = await Promise.all( filtered.map(async (u) => { const [m] = await db .select({ orgId: memberships.orgId, orgName: organizations.name, orgSlug: organizations.slug, plan: organizations.plan, role: memberships.role, }) .from(memberships) .innerJoin(organizations, eq(organizations.id, memberships.orgId)) .where(eq(memberships.userId, u.id)) .limit(1); const [sc] = await db .select({ c: count() }) .from(mcpServers) .where(m ? eq(mcpServers.orgId, m.orgId) : sql`false`); return { id: u.id, email: u.email, name: u.name, isAdmin: u.isAdmin, emailVerified: u.emailVerified, lastLoginAt: u.lastLoginAt, createdAt: u.createdAt, org: m ?? null, serverCount: Number(sc?.c ?? 0), }; }), ); return reply.send({ users: enriched }); }); app.patch('/v1/admin/users/:id', { preHandler: requireAdmin }, async (req, reply) => { const Params = z.object({ id: z.string().uuid() }); const Body = z.object({ isAdmin: z.boolean().optional(), name: z.string().optional(), }); const p = Params.safeParse(req.params); const b = Body.safeParse(req.body); if (!p.success || !b.success) return reply.code(400).send({ error: 'invalid_input' }); await db.update(users).set(b.data).where(eq(users.id, p.data.id)); await audit({ orgId: req.user!.orgId, userId: req.user!.userId, action: 'admin.user.update', resourceType: 'user', resourceId: p.data.id, metadata: b.data, ipAddress: req.ip, }); return reply.send({ ok: true }); }); app.delete('/v1/admin/users/:id', { preHandler: requireAdmin }, async (req, reply) => { const Params = z.object({ id: z.string().uuid() }); const p = Params.safeParse(req.params); if (!p.success) return reply.code(400).send({ error: 'invalid_id' }); if (p.data.id === req.user!.userId) { return reply.code(400).send({ error: 'cannot_delete_self' }); } await db.delete(users).where(eq(users.id, p.data.id)); await audit({ orgId: req.user!.orgId, userId: req.user!.userId, action: 'admin.user.delete', resourceType: 'user', resourceId: p.data.id, ipAddress: req.ip, }); return reply.send({ ok: true }); }); // ---- Organizations ---- app.get('/v1/admin/orgs', { preHandler: requireAdmin }, async (_req, reply) => { const rows = await db.select().from(organizations).orderBy(desc(organizations.createdAt)); const enriched = await Promise.all( rows.map(async (o) => { const [mc] = await db .select({ c: count() }) .from(memberships) .where(eq(memberships.orgId, o.id)); const [sc] = await db .select({ c: count() }) .from(mcpServers) .where(eq(mcpServers.orgId, o.id)); return { ...o, memberCount: Number(mc?.c ?? 0), serverCount: Number(sc?.c ?? 0), }; }), ); return reply.send({ orgs: enriched }); }); app.patch('/v1/admin/orgs/:id', { preHandler: requireAdmin }, async (req, reply) => { const Params = z.object({ id: z.string().uuid() }); const Body = z.object({ plan: z.enum(['hobby', 'pro', 'team', 'enterprise']).optional(), monthlyCallQuota: z.coerce.number().int().min(0).optional(), suspended: z.boolean().optional(), suspendedReason: z.string().max(500).nullable().optional(), }); const p = Params.safeParse(req.params); const b = Body.safeParse(req.body); if (!p.success || !b.success) return reply.code(400).send({ error: 'invalid_input' }); await db.update(organizations).set(b.data).where(eq(organizations.id, p.data.id)); // If suspended, pause all the org's servers if (b.data.suspended === true) { await db .update(mcpServers) .set({ status: 'paused', updatedAt: new Date() }) .where(eq(mcpServers.orgId, p.data.id)); } await audit({ orgId: req.user!.orgId, userId: req.user!.userId, action: 'admin.org.update', resourceType: 'org', resourceId: p.data.id, metadata: b.data, ipAddress: req.ip, }); return reply.send({ ok: true }); }); // ---- Servers ---- app.get('/v1/admin/servers', { preHandler: requireAdmin }, async (req, reply) => { const Query = z.object({ status: z.string().optional(), limit: z.coerce.number().min(1).max(500).default(200), }); const parsed = Query.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'invalid_query' }); const rows = await db .select({ server: mcpServers, org: { id: organizations.id, name: organizations.name, slug: organizations.slug, plan: organizations.plan }, }) .from(mcpServers) .innerJoin(organizations, eq(organizations.id, mcpServers.orgId)) .orderBy(desc(mcpServers.updatedAt)) .limit(parsed.data.limit); const filtered = parsed.data.status ? rows.filter((r) => r.server.status === parsed.data.status) : rows; return reply.send({ servers: filtered }); }); app.post('/v1/admin/servers/:id/rebuild', { preHandler: requireAdmin }, async (req, reply) => { const Params = z.object({ id: z.string().uuid() }); const p = Params.safeParse(req.params); if (!p.success) return reply.code(400).send({ error: 'invalid_id' }); const [server] = await db.select().from(mcpServers).where(eq(mcpServers.id, p.data.id)).limit(1); if (!server) return reply.code(404).send({ error: 'not_found' }); // Get last build's prompt const [lastBuild] = await db .select() .from(builds) .where(eq(builds.serverId, server.id)) .orderBy(desc(builds.version)) .limit(1); if (!lastBuild) return reply.code(400).send({ error: 'no_previous_build' }); const nextVersion = (server.currentVersion ?? 0) + 1; const [build] = await db .insert(builds) .values({ serverId: server.id, version: nextVersion, prompt: lastBuild.prompt, status: 'queued', }) .returning(); if (!build) return reply.code(500).send({ error: 'build_create_failed' }); await db .update(mcpServers) .set({ status: 'queued', updatedAt: new Date() }) .where(eq(mcpServers.id, server.id)); await getBuildQueue().add('generate', { buildId: build.id, serverId: server.id, orgId: server.orgId, prompt: lastBuild.prompt, version: nextVersion, slug: server.slug, serverName: server.name, secrets: {}, }); await audit({ orgId: req.user!.orgId, userId: req.user!.userId, action: 'admin.server.rebuild', resourceType: 'server', resourceId: server.id, metadata: { newVersion: nextVersion }, ipAddress: req.ip, }); return reply.send({ build }); }); app.delete('/v1/admin/servers/:id', { preHandler: requireAdmin }, async (req, reply) => { const Params = z.object({ id: z.string().uuid() }); const p = Params.safeParse(req.params); if (!p.success) return reply.code(400).send({ error: 'invalid_id' }); const [server] = await db.select().from(mcpServers).where(eq(mcpServers.id, p.data.id)).limit(1); if (!server) return reply.code(404).send({ error: 'not_found' }); await db.delete(mcpServers).where(eq(mcpServers.id, server.id)); await audit({ orgId: req.user!.orgId, userId: req.user!.userId, action: 'admin.server.delete', resourceType: 'server', resourceId: server.id, metadata: { slug: server.slug, name: server.name }, ipAddress: req.ip, }); return reply.send({ ok: true }); }); // ---- Builds ---- app.get('/v1/admin/builds', { preHandler: requireAdmin }, async (req, reply) => { const Query = z.object({ status: z.string().optional(), limit: z.coerce.number().min(1).max(500).default(100), }); const parsed = Query.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'invalid_query' }); const rows = await db .select({ build: builds, server: { id: mcpServers.id, name: mcpServers.name, slug: mcpServers.slug }, org: { id: organizations.id, name: organizations.name }, }) .from(builds) .innerJoin(mcpServers, eq(mcpServers.id, builds.serverId)) .innerJoin(organizations, eq(organizations.id, mcpServers.orgId)) .orderBy(desc(builds.createdAt)) .limit(parsed.data.limit); const filtered = parsed.data.status ? rows.filter((r) => r.build.status === parsed.data.status) : rows; return reply.send({ builds: filtered }); }); app.get('/v1/admin/builds/:id/logs', { preHandler: requireAdmin }, async (req, reply) => { const Params = z.object({ id: z.string().uuid() }); const p = Params.safeParse(req.params); if (!p.success) return reply.code(400).send({ error: 'invalid_id' }); const logs = await db .select() .from(buildLogs) .where(eq(buildLogs.buildId, p.data.id)) .orderBy(buildLogs.timestamp); return reply.send({ logs }); }); // ---- Audit (system-wide) ---- app.get('/v1/admin/audit', { preHandler: requireAdmin }, async (req, reply) => { const Query = z.object({ action: z.string().optional(), resourceType: z.string().optional(), limit: z.coerce.number().min(1).max(1000).default(200), }); const parsed = Query.safeParse(req.query); if (!parsed.success) return reply.code(400).send({ error: 'invalid_query' }); let rows = await db .select({ entry: auditLog, userEmail: users.email, }) .from(auditLog) .leftJoin(users, eq(users.id, auditLog.userId)) .orderBy(desc(auditLog.createdAt)) .limit(parsed.data.limit); if (parsed.data.action) { rows = rows.filter((r) => r.entry.action === parsed.data.action); } if (parsed.data.resourceType) { rows = rows.filter((r) => r.entry.resourceType === parsed.data.resourceType); } return reply.send({ entries: rows }); }); // ---- System health ---- app.get('/v1/admin/system', { preHandler: requireAdmin }, async (_req, reply) => { const t0 = Date.now(); // DB probe let dbOk = false; let dbLatency: number | null = null; try { const start = Date.now(); await db.execute(sql`SELECT 1`); dbLatency = Date.now() - start; dbOk = true; } catch { // remains false } // Redis probe let redisOk = false; let redisLatency: number | null = null; let queueDepth: number | null = null; try { const start = Date.now(); const pong = await getRedis().ping(); redisLatency = Date.now() - start; redisOk = pong === 'PONG'; const q = getBuildQueue(); const counts = await q.getJobCounts('waiting', 'active', 'completed', 'failed', 'delayed'); queueDepth = (counts.waiting ?? 0) + (counts.active ?? 0) + (counts.delayed ?? 0); } catch { // remains false } // Docker container count const containerCount = await new Promise((resolve) => { const child = spawn('docker', ['ps', '--filter', 'name=bmm-mcp-', '-q'], { stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32', }); let out = ''; child.stdout?.on('data', (d: Buffer) => { out += d.toString(); }); child.on('close', (code) => { if (code !== 0) { resolve(null); return; } const lines = out .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); resolve(lines.length); }); child.on('error', () => resolve(null)); }); return reply.send({ probedAtMs: Date.now() - t0, db: { ok: dbOk, latencyMs: dbLatency }, redis: { ok: redisOk, latencyMs: redisLatency, queueDepth }, docker: { containerCount }, }); }); // ---- System prompt (read + override) ---- app.get('/v1/admin/prompt', { preHandler: requireAdmin }, async (_req, reply) => { const [row] = await db .select() .from(adminSettings) .where(eq(adminSettings.key, PROMPT_KEY)) .limit(1); return reply.send({ builtin: SYSTEM_PROMPT, override: row?.value ?? null, updatedAt: row?.updatedAt ?? null, }); }); app.patch('/v1/admin/prompt', { preHandler: requireAdmin }, async (req, reply) => { const Body = z.object({ value: z.string().min(1).max(20_000).nullable(), }); const parsed = Body.safeParse(req.body); if (!parsed.success) return reply.code(400).send({ error: 'invalid_input' }); if (parsed.data.value === null) { await db.delete(adminSettings).where(eq(adminSettings.key, PROMPT_KEY)); } else { const [existing] = await db .select() .from(adminSettings) .where(eq(adminSettings.key, PROMPT_KEY)) .limit(1); if (existing) { await db .update(adminSettings) .set({ value: parsed.data.value, updatedAt: new Date(), updatedBy: req.user!.userId, }) .where(eq(adminSettings.id, existing.id)); } else { await db.insert(adminSettings).values({ key: PROMPT_KEY, value: parsed.data.value, updatedBy: req.user!.userId, }); } } await audit({ orgId: req.user!.orgId, userId: req.user!.userId, action: 'admin.prompt.update', resourceType: 'admin_settings', resourceId: PROMPT_KEY, metadata: { hasOverride: parsed.data.value !== null }, ipAddress: req.ip, }); return reply.send({ ok: true }); }); void inArray; // referenced for future bulk operations }