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:
2026-03-01 02:45:33 +00:00
parent 31cb36fb77
commit e67cef518e
9 changed files with 486 additions and 0 deletions

View 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);
});
}