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.
This commit is contained in:
31
apps/api/src/lib/audit.ts
Normal file
31
apps/api/src/lib/audit.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { auditLog, createDb } from '@bmm/db';
|
||||
|
||||
const db = createDb();
|
||||
|
||||
export interface AuditInput {
|
||||
orgId?: string;
|
||||
userId?: string;
|
||||
action: string;
|
||||
resourceType?: string;
|
||||
resourceId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
export async function audit(input: AuditInput): Promise<void> {
|
||||
try {
|
||||
await db.insert(auditLog).values({
|
||||
orgId: input.orgId ?? null,
|
||||
userId: input.userId ?? null,
|
||||
action: input.action,
|
||||
resourceType: input.resourceType ?? null,
|
||||
resourceId: input.resourceId ?? null,
|
||||
metadata: input.metadata ?? null,
|
||||
ipAddress: input.ipAddress ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
// Audit failures must never block the request path.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[audit] failed to write entry:', err);
|
||||
}
|
||||
}
|
||||
25
apps/api/src/lib/preview-cache.ts
Normal file
25
apps/api/src/lib/preview-cache.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { getRedis } from './redis.js';
|
||||
import type { GeneratorSpec } from '@bmm/types';
|
||||
|
||||
const TTL_SECONDS = 5 * 60;
|
||||
|
||||
function key(previewId: string): string {
|
||||
return `preview:${previewId}`;
|
||||
}
|
||||
|
||||
export async function cacheSpec(spec: GeneratorSpec): Promise<string> {
|
||||
const previewId = crypto.randomBytes(12).toString('base64url');
|
||||
await getRedis().set(key(previewId), JSON.stringify(spec), 'EX', TTL_SECONDS);
|
||||
return previewId;
|
||||
}
|
||||
|
||||
export async function loadSpec(previewId: string): Promise<GeneratorSpec | null> {
|
||||
const raw = await getRedis().get(key(previewId));
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as GeneratorSpec;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface BuildJobData {
|
||||
slug: string;
|
||||
serverName: string;
|
||||
secrets: Record<string, string>;
|
||||
previewId?: string;
|
||||
}
|
||||
|
||||
let queue: Queue<BuildJobData> | null = null;
|
||||
|
||||
Reference in New Issue
Block a user