diff --git a/products/02-iac-drift-detection/saas/src/api/routes.ts b/products/02-iac-drift-detection/saas/src/api/routes.ts index 0d305a3..3849752 100644 --- a/products/02-iac-drift-detection/saas/src/api/routes.ts +++ b/products/02-iac-drift-detection/saas/src/api/routes.ts @@ -1,7 +1,59 @@ import { FastifyInstance } from 'fastify'; +import { z } from 'zod'; +import crypto from 'crypto'; import { withTenant } from '../data/db.js'; +const submitReportSchema = z.object({ + stack_name: z.string().min(1).max(200), + stack_fingerprint: z.string().min(1), + agent_version: z.string().default('manual'), + scanned_at: z.string().datetime().optional(), + state_serial: z.number().int().min(0), + lineage: z.string().default('manual'), + total_resources: z.number().int().min(0), + drift_score: z.number().min(0).max(100), + raw_report: z.record(z.unknown()).default({}), +}); + export async function registerApiRoutes(app: FastifyInstance) { + // Submit a drift report (from agent or manual) + app.post('/api/v1/reports', async (request, reply) => { + const tenantId = (request as any).tenantId; + const pool = (app as any).pool; + const body = submitReportSchema.parse(request.body); + const nonce = crypto.randomUUID(); + + const result = await withTenant(pool, tenantId, async (client) => { + const { rows } = await client.query( + `INSERT INTO drift_reports (tenant_id, stack_name, stack_fingerprint, agent_version, scanned_at, state_serial, lineage, total_resources, drift_score, nonce, raw_report) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + RETURNING id, stack_name, drift_score, scanned_at`, + [tenantId, body.stack_name, body.stack_fingerprint, body.agent_version, + body.scanned_at ?? new Date().toISOString(), body.state_serial, body.lineage, + body.total_resources, body.drift_score, nonce, JSON.stringify(body.raw_report)] + ); + return rows[0]; + }); + + return reply.status(201).send({ report: result }); + }); + + // Delete all reports for a stack + app.delete('/api/v1/stacks/:stackName', async (request, reply) => { + const tenantId = (request as any).tenantId; + const { stackName } = request.params as { stackName: string }; + const pool = (app as any).pool; + + await withTenant(pool, tenantId, async (client) => { + await client.query( + 'DELETE FROM drift_reports WHERE tenant_id = $1 AND stack_name = $2', + [tenantId, stackName] + ); + }); + + return reply.status(200).send({ status: 'deleted', stack_name: stackName }); + }); + // List stacks app.get('/api/v1/stacks', async (request, reply) => { const tenantId = (request as any).tenantId;