- 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.
58 lines
1.9 KiB
TypeScript
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 });
|
|
});
|
|
}
|