Scaffold dd0c/drift SaaS backend: Fastify, RLS, ingestion, dashboard API
- Fastify server with Zod validation, pino logging, CORS/helmet - Drift report ingestion endpoint with nonce replay prevention - Dashboard API: stacks list, drift history, report detail, summary stats - PostgreSQL schema with RLS: tenants, users, agent_keys, drift_reports, remediation_actions - withTenant() helper for safe connection pool tenant context management - Config via Zod-validated env vars
This commit is contained in:
102
products/02-iac-drift-detection/saas/src/api/routes.ts
Normal file
102
products/02-iac-drift-detection/saas/src/api/routes.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { withTenant } from '../data/db.js';
|
||||
|
||||
export async function registerApiRoutes(app: FastifyInstance) {
|
||||
// List stacks
|
||||
app.get('/api/v1/stacks', async (request, reply) => {
|
||||
const tenantId = (request as any).tenantId;
|
||||
const pool = (app as any).pool;
|
||||
|
||||
const result = await withTenant(pool, tenantId, async (client) => {
|
||||
const { rows } = await client.query(
|
||||
`SELECT DISTINCT ON (stack_name)
|
||||
stack_name, stack_fingerprint, drift_score, total_resources, scanned_at, state_serial
|
||||
FROM drift_reports
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY stack_name, scanned_at DESC`,
|
||||
[tenantId]
|
||||
);
|
||||
return rows;
|
||||
});
|
||||
|
||||
return reply.send(result);
|
||||
});
|
||||
|
||||
// Get stack drift history
|
||||
app.get('/api/v1/stacks/:stackName/history', async (request, reply) => {
|
||||
const tenantId = (request as any).tenantId;
|
||||
const { stackName } = request.params as { stackName: string };
|
||||
const pool = (app as any).pool;
|
||||
|
||||
const result = await withTenant(pool, tenantId, async (client) => {
|
||||
const { rows } = await client.query(
|
||||
`SELECT id, drift_score, total_resources, scanned_at, state_serial,
|
||||
(raw_report->'drifted_resources') as drifted_resources
|
||||
FROM drift_reports
|
||||
WHERE tenant_id = $1 AND stack_name = $2
|
||||
ORDER BY scanned_at DESC
|
||||
LIMIT 50`,
|
||||
[tenantId, stackName]
|
||||
);
|
||||
return rows;
|
||||
});
|
||||
|
||||
return reply.send(result);
|
||||
});
|
||||
|
||||
// Get single drift report
|
||||
app.get('/api/v1/reports/:reportId', async (request, reply) => {
|
||||
const tenantId = (request as any).tenantId;
|
||||
const { reportId } = request.params as { reportId: string };
|
||||
const pool = (app as any).pool;
|
||||
|
||||
const result = await withTenant(pool, tenantId, async (client) => {
|
||||
const { rows } = await client.query(
|
||||
`SELECT * FROM drift_reports WHERE tenant_id = $1 AND id = $2`,
|
||||
[tenantId, reportId]
|
||||
);
|
||||
return rows[0] ?? null;
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return reply.status(404).send({ error: 'Not found' });
|
||||
}
|
||||
|
||||
return reply.send(result);
|
||||
});
|
||||
|
||||
// Dashboard summary
|
||||
app.get('/api/v1/dashboard', async (request, reply) => {
|
||||
const tenantId = (request as any).tenantId;
|
||||
const pool = (app as any).pool;
|
||||
|
||||
const result = await withTenant(pool, tenantId, async (client) => {
|
||||
const stacks = await client.query(
|
||||
`SELECT COUNT(DISTINCT stack_name) as stack_count FROM drift_reports WHERE tenant_id = $1`,
|
||||
[tenantId]
|
||||
);
|
||||
const drifted = await client.query(
|
||||
`SELECT COUNT(DISTINCT stack_name) as drifted_count
|
||||
FROM drift_reports
|
||||
WHERE tenant_id = $1 AND drift_score > 0
|
||||
AND scanned_at = (SELECT MAX(scanned_at) FROM drift_reports r2 WHERE r2.stack_name = drift_reports.stack_name AND r2.tenant_id = $1)`,
|
||||
[tenantId]
|
||||
);
|
||||
const critical = await client.query(
|
||||
`SELECT COUNT(*) as critical_count
|
||||
FROM drift_reports
|
||||
WHERE tenant_id = $1 AND drift_score >= 80
|
||||
AND scanned_at >= NOW() - INTERVAL '24 hours'`,
|
||||
[tenantId]
|
||||
);
|
||||
|
||||
return {
|
||||
total_stacks: parseInt(stacks.rows[0]?.stack_count ?? '0'),
|
||||
drifted_stacks: parseInt(drifted.rows[0]?.drifted_count ?? '0'),
|
||||
critical_last_24h: parseInt(critical.rows[0]?.critical_count ?? '0'),
|
||||
};
|
||||
});
|
||||
|
||||
return reply.send(result);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user