From 9f46b842578e3ed10612175cb6232399fa3a88f9 Mon Sep 17 00:00:00 2001 From: Max Mayfield Date: Mon, 2 Mar 2026 05:00:40 +0000 Subject: [PATCH] Add API to manage webhook secrets for alert integrations --- .../src/api/webhook_secrets.ts | 49 +++++++++++++++++++ products/03-alert-intelligence/src/index.ts | 2 + 2 files changed, 51 insertions(+) create mode 100644 products/03-alert-intelligence/src/api/webhook_secrets.ts diff --git a/products/03-alert-intelligence/src/api/webhook_secrets.ts b/products/03-alert-intelligence/src/api/webhook_secrets.ts new file mode 100644 index 0000000..904b589 --- /dev/null +++ b/products/03-alert-intelligence/src/api/webhook_secrets.ts @@ -0,0 +1,49 @@ +import type { FastifyInstance } from 'fastify'; +import { z } from 'zod'; +import { withTenant } from '../data/db.js'; + +const createSecretSchema = z.object({ + provider: z.enum(['datadog', 'pagerduty', 'opsgenie', 'grafana', 'custom']), + secret: z.string().min(1).max(500), +}); + +export function registerWebhookSecretRoutes(app: FastifyInstance) { + // List webhook secrets + app.get('/api/v1/webhooks/secrets', async (req, reply) => { + const tenantId = (req as any).tenantId; + const result = await withTenant(tenantId, async (client) => { + return client.query('SELECT provider, created_at FROM webhook_secrets ORDER BY provider'); + }); + return { secrets: result.rows }; + }); + + // Create or update webhook secret + app.put('/api/v1/webhooks/secrets', async (req, reply) => { + const tenantId = (req as any).tenantId; + const { provider, secret } = createSecretSchema.parse(req.body); + + await withTenant(tenantId, async (client) => { + await client.query( + `INSERT INTO webhook_secrets (tenant_id, provider, secret) + VALUES ($1, $2, $3) + ON CONFLICT (tenant_id, provider) DO UPDATE + SET secret = EXCLUDED.secret, created_at = now()`, + [tenantId, provider, secret] + ); + }); + + return reply.status(200).send({ status: 'success', provider }); + }); + + // Delete webhook secret + app.delete('/api/v1/webhooks/secrets/:provider', async (req, reply) => { + const tenantId = (req as any).tenantId; + const { provider } = req.params as { provider: string }; + + await withTenant(tenantId, async (client) => { + await client.query('DELETE FROM webhook_secrets WHERE provider = $1', [provider]); + }); + + return reply.status(200).send({ status: 'success', deleted: provider }); + }); +} diff --git a/products/03-alert-intelligence/src/index.ts b/products/03-alert-intelligence/src/index.ts index 898d36c..9e9ce2d 100644 --- a/products/03-alert-intelligence/src/index.ts +++ b/products/03-alert-intelligence/src/index.ts @@ -6,6 +6,7 @@ import { config } from './config/index.js'; import { pool } from './data/db.js'; import { registerAuth, registerAuthRoutes } from './auth/middleware.js'; import { registerWebhookRoutes } from './api/webhooks.js'; +import { registerWebhookSecretRoutes } from './api/webhook_secrets.js'; import { registerIncidentRoutes } from './api/incidents.js'; import { registerNotificationRoutes } from './api/notifications.js'; @@ -22,6 +23,7 @@ app.get('/health', async () => ({ status: 'ok', service: 'dd0c-alert' })); registerAuthRoutes(app, config.JWT_SECRET, pool); registerWebhookRoutes(app); + registerWebhookSecretRoutes(app); registerIncidentRoutes(app); registerNotificationRoutes(app);