buildmymcpserver/apps/api/src/routes/settings.ts
Marco Sadjadi 1c92964bbd feat(api,generator): preview endpoint + spec cache + audit-log writes
- POST /v1/servers/preview runs Claude synchronously, validates output, caches spec
  in Redis under preview:<id> with 5min TTL, returns previewId+spec+detectedSecrets.
- POST /v1/servers accepts optional previewId; worker reuses the cached spec if
  the entry is still present, otherwise regenerates fresh. Skips the second
  Claude round-trip (~30s saved on the demoable path).
- audit() helper writes auth.login, auth.logout, server.create, server.iterate,
  server.delete to audit_log with ip, metadata, resourceId.
- GET /v1/me/org returns organization + members list for the settings page.
- GET /v1/audit?limit=&action=&resourceType= returns scoped audit entries.
2026-05-19 18:08:29 +02:00

58 lines
1.9 KiB
TypeScript

import type { FastifyInstance } from 'fastify';
import { z } from 'zod';
import { auditLog, createDb, desc, eq, memberships, organizations, users } from '@bmm/db';
import { requireAuth } from '../plugins/session.js';
const db = createDb();
export async function settingsRoutes(app: FastifyInstance): Promise<void> {
app.get('/v1/me/org', { preHandler: requireAuth }, async (req, reply) => {
const user = req.user!;
const [org] = await db.select().from(organizations).where(eq(organizations.id, user.orgId)).limit(1);
if (!org) return reply.code(404).send({ error: 'not_found' });
const members = await db
.select({
id: memberships.id,
userId: users.id,
email: users.email,
name: users.name,
role: memberships.role,
createdAt: memberships.createdAt,
})
.from(memberships)
.innerJoin(users, eq(users.id, memberships.userId))
.where(eq(memberships.orgId, org.id))
.orderBy(memberships.createdAt);
return reply.send({ org, members });
});
app.get('/v1/audit', { preHandler: requireAuth }, async (req, reply) => {
const user = req.user!;
const Query = z.object({
limit: z.coerce.number().min(1).max(500).default(100),
action: z.string().optional(),
resourceType: z.string().optional(),
});
const parsed = Query.safeParse(req.query);
if (!parsed.success) return reply.code(400).send({ error: 'invalid_query' });
let rows = await db
.select()
.from(auditLog)
.where(eq(auditLog.orgId, user.orgId))
.orderBy(desc(auditLog.createdAt))
.limit(parsed.data.limit);
if (parsed.data.action) {
rows = rows.filter((r) => r.action === parsed.data.action);
}
if (parsed.data.resourceType) {
rows = rows.filter((r) => r.resourceType === parsed.data.resourceType);
}
return reply.send({ entries: rows });
});
}