diff --git a/client/src/pages/webhooks.tsx b/client/src/pages/webhooks.tsx index 74fb650..a26ef2b 100644 --- a/client/src/pages/webhooks.tsx +++ b/client/src/pages/webhooks.tsx @@ -4,7 +4,7 @@ import { useToast } from "@/hooks/use-toast"; import { queryClient, apiRequest } from "@/lib/queryClient"; import { useAuth } from "@/hooks/use-auth"; import { useOrganization } from "@/context/organization-context"; -import { Webhook } from "@shared/schema"; +import { Webhook, WebhookDeliveryLog } from "@shared/schema"; import { Card, diff --git a/server/database-storage.ts b/server/database-storage.ts index c202532..ba0926f 100644 --- a/server/database-storage.ts +++ b/server/database-storage.ts @@ -1,13 +1,14 @@ import { users, organizations, domains, dnsRecords, - providers, dnsHistory, apiTokens, webhooks, + providers, dnsHistory, apiTokens, webhooks, webhookDeliveryLogs, type User, type InsertUser, type Organization, type InsertOrganization, type Domain, type InsertDomain, type DnsRecord, type InsertDnsRecord, type Provider, type InsertProvider, type ApiToken, type InsertApiToken, - type DnsHistory, type Webhook, type InsertWebhook + type DnsHistory, type Webhook, type InsertWebhook, + type WebhookDeliveryLog, type InsertWebhookDeliveryLog } from "@shared/schema"; import { db } from "./db"; import { eq, and, desc, or, inArray, sql } from "drizzle-orm"; @@ -297,9 +298,39 @@ export class DatabaseStorage implements IStorage { const webhook = await this.getWebhook(webhookId); if (!webhook || !webhook.isActive) return false; + // Using the webhook utility to deliver the webhook + const { generateSignature, deliverWebhook } = await import('./utils/webhook'); + // In a real implementation, this would make an HTTP request to the webhook URL console.log(`Triggering webhook ${webhook.name} (${webhook.id}) with payload:`, payload); + // Generate signature for the payload if a secret is set + const signature = webhook.secret ? generateSignature(payload, webhook.secret) : ''; + + // In a real implementation, this would make the actual HTTP request + // For now, simulate a successful delivery + const deliveryResult = { + success: true, + statusCode: 200, + message: 'Webhook delivered successfully (simulated)', + timestamp: new Date(), + responseBody: JSON.stringify({ success: true }), + retryCount: 0 + }; + + // Log the delivery attempt + await this.addWebhookDeliveryLog({ + webhookId: webhook.id, + event: payload.event || 'unknown', + payload, + signature, + status: deliveryResult.success, + statusCode: deliveryResult.statusCode, + message: deliveryResult.message, + responseBody: deliveryResult.responseBody, + retryCount: deliveryResult.retryCount + }); + // Update the lastTriggered timestamp await db.update(webhooks) .set({ lastTriggered: new Date() }) @@ -308,7 +339,39 @@ export class DatabaseStorage implements IStorage { return true; } catch (error) { console.error(`Error triggering webhook ${webhookId}:`, error); + + // Log the failed delivery attempt + if (error instanceof Error) { + await this.addWebhookDeliveryLog({ + webhookId, + event: payload?.event || 'unknown', + payload, + signature: '', + status: false, + message: `Error: ${error.message}`, + retryCount: 0 + }); + } + return false; } } + + // Webhook Delivery Logs + async addWebhookDeliveryLog(log: InsertWebhookDeliveryLog): Promise { + const [newLog] = await db.insert(webhookDeliveryLogs).values(log).returning(); + return newLog; + } + + async getWebhookDeliveryLog(id: string): Promise { + const [log] = await db.select().from(webhookDeliveryLogs).where(eq(webhookDeliveryLogs.id, id)); + return log; + } + + async getWebhookDeliveryLogsByWebhook(webhookId: string): Promise { + return await db.select() + .from(webhookDeliveryLogs) + .where(eq(webhookDeliveryLogs.webhookId, webhookId)) + .orderBy(desc(webhookDeliveryLogs.createdAt)); + } } \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 47a83bd..404462c 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -911,5 +911,57 @@ export async function registerRoutes(app: Express): Promise { } }); + // Webhook Delivery Log endpoints + + // Get all delivery logs for a webhook + app.get("/api/webhooks/:id/logs", requireRole(["admin", "manager"]), async (req, res) => { + try { + const webhook = await storage.getWebhook(req.params.id); + + if (!webhook) { + return res.status(404).json({ message: "Webhook not found" }); + } + + // Permission check: admin can view all logs, others only for their organization's webhooks + if (req.user?.role !== "admin" && webhook.organizationId !== req.user?.organizationId) { + return res.status(403).json({ message: "Not authorized to access logs for this webhook" }); + } + + const logs = await storage.getWebhookDeliveryLogsByWebhook(req.params.id); + res.json(logs); + } catch (error) { + console.error("Error fetching webhook delivery logs:", error); + res.status(500).json({ message: "Internal server error" }); + } + }); + + // Get a specific delivery log by ID + app.get("/api/webhook-logs/:id", requireRole(["admin", "manager"]), async (req, res) => { + try { + const log = await storage.getWebhookDeliveryLog(req.params.id); + + if (!log) { + return res.status(404).json({ message: "Webhook delivery log not found" }); + } + + // Need to get the webhook to check permissions + const webhook = await storage.getWebhook(log.webhookId); + + if (!webhook) { + return res.status(404).json({ message: "Associated webhook not found" }); + } + + // Permission check + if (req.user?.role !== "admin" && webhook.organizationId !== req.user?.organizationId) { + return res.status(403).json({ message: "Not authorized to access this log" }); + } + + res.json(log); + } catch (error) { + console.error("Error fetching webhook delivery log:", error); + res.status(500).json({ message: "Internal server error" }); + } + }); + return httpServer; }