buildmymcpserver/apps/api/src/index.ts
Marco Sadjadi c62fcd07ef feat(admin): password-auth admin panel with 8 pages + 15 API endpoints
Schema migrations:
- users.is_admin boolean
- users.password_hash text (scrypt N=16384, 16-byte salt)
- users.last_login_at timestamp
- organizations.suspended + suspended_reason
- admin_settings table (DB-stored prompt override + future settings)

Auth (@bmm/auth):
- hashPassword + verifyPassword via node:crypto scrypt (no extra dep)
- loginWithPassword: scrypt-verifies, issues 30-day session, updates last_login_at
- seedAdmin: idempotent upsert keyed on email; creates org + membership on first run
- AuthedUser now carries isAdmin flag

API:
- POST /v1/auth/admin/login (email + password) — 300ms throttle on failure
- requireAdmin preHandler — 401 if no session, 403 if non-admin
- Bootstrap: api on boot calls seedAdmin(ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_NAME)
  if env present. Idempotent.

Admin API routes (all gated by requireAdmin):
- GET /v1/admin/overview (totals, trends 7d, server-status breakdown, builds 24h, recent activity)
- GET /v1/admin/users (search, per-row org + plan + serverCount)
- PATCH /v1/admin/users/:id (isAdmin, name)
- DELETE /v1/admin/users/:id (self-delete blocked)
- GET /v1/admin/orgs (member + server counts)
- PATCH /v1/admin/orgs/:id (plan, quota, suspended; cascades to mcp_servers.status=paused on suspend)
- GET /v1/admin/servers (cross-org with status filter)
- POST /v1/admin/servers/:id/rebuild (re-queues build using last prompt)
- DELETE /v1/admin/servers/:id
- GET /v1/admin/builds (status filter, error messages, prompt previews)
- GET /v1/admin/builds/:id/logs
- GET /v1/admin/audit (system-wide with user email join)
- GET /v1/admin/system (DB ping, Redis ping, BullMQ queue depth, docker ps count)
- GET /v1/admin/prompt (builtin + override + updatedAt)
- PATCH /v1/admin/prompt (value: string | null) — saves DB override or drops it

UI (apps/web/app/admin/*):
- /admin/login — password form, separate from /login magic-link
- AdminLayout — Linear-style sidebar (8 nav items), bottom panel with user email +
  'user view' shortcut + logout, client-side requireAdmin guard with redirect
- /admin — overview dashboard with 4 metric cards, 2 panels (status + 24h builds),
  recent activity table linking to full audit
- /admin/users — search + admin toggle + delete (self-delete blocked)
- /admin/orgs — plan/quota/suspend actions via prompts
- /admin/servers — cross-org table with rebuild + delete actions, status filter
- /admin/builds — every build cross-fleet with error vs prompt preview
- /admin/audit — system-wide log + CSV export + filter dropdowns
- /admin/system — auto-refreshing 5s health probes for Postgres, Redis, queue, Docker
- /admin/prompt — live editor for the LLM system prompt with built-in baseline,
  override-state badge, drop-override action, diff preview, save-as-override

End-to-end verified: login as marco.frangiskatos@gmail.com + Melusa112233.*, every
admin page returns 200, admin login + overview tested via screenshot, docker probe
returns true count of running MCP containers.
2026-05-19 23:01:26 +02:00

65 lines
1.9 KiB
TypeScript

import Fastify from 'fastify';
import cors from '@fastify/cors';
import cookie from '@fastify/cookie';
import websocket from '@fastify/websocket';
import { seedAdmin } from '@bmm/auth';
import { config } from './config.js';
import { authRoutes } from './routes/auth.js';
import { serverRoutes } from './routes/servers.js';
import { oauthRoutes } from './routes/oauth.js';
import { settingsRoutes } from './routes/settings.js';
import { adminRoutes } from './routes/admin.js';
const app = Fastify({
logger: {
level: config.NODE_ENV === 'production' ? 'info' : 'debug',
},
});
await app.register(cors, {
origin: [config.NEXT_PUBLIC_APP_URL],
credentials: true,
});
await app.register(cookie);
await app.register(websocket, { options: { maxPayload: 1024 * 1024 } });
app.get('/health', async () => ({ ok: true, ts: Date.now() }));
await app.register(authRoutes);
await app.register(serverRoutes);
await app.register(oauthRoutes);
await app.register(settingsRoutes);
await app.register(adminRoutes);
// Bootstrap admin user from env (idempotent)
if (config.ADMIN_EMAIL && config.ADMIN_PASSWORD) {
try {
const result = await seedAdmin({
email: config.ADMIN_EMAIL,
password: config.ADMIN_PASSWORD,
name: config.ADMIN_NAME,
});
app.log.info(
{ email: config.ADMIN_EMAIL, created: result.created },
`[admin-seed] ${result.created ? 'created' : 'updated'} admin user`,
);
} catch (err) {
app.log.error({ err }, '[admin-seed] failed to seed admin user');
}
}
app.setErrorHandler((err, _req, reply) => {
app.log.error(err);
if (!reply.sent) {
reply.code(err.statusCode ?? 500).send({ error: err.message ?? 'internal_error' });
}
});
try {
await app.listen({ port: config.PORT, host: '0.0.0.0' });
app.log.info(`api listening on http://localhost:${config.PORT}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}