mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-09 01:39:25 +00:00
Implement webhook notifications for DNS changes. This includes adding webhook creation and management endpoints and triggering webhooks on DNS record creation, updates, and deletions.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/8073f1fe-e0d9-429c-8016-8ed7ce338f7f.jpg
This commit is contained in:
@@ -9,11 +9,58 @@ import {
|
||||
insertProviderSchema,
|
||||
insertApiTokenSchema,
|
||||
insertOrganizationSchema,
|
||||
insertWebhookSchema,
|
||||
recordTypes,
|
||||
providerTypes
|
||||
} from "@shared/schema";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
// Helper function to trigger webhooks for DNS operations
|
||||
async function triggerDnsWebhooks(
|
||||
action: string,
|
||||
domainId: string,
|
||||
recordData: any,
|
||||
previousData: any = null,
|
||||
userId: string | null = null
|
||||
) {
|
||||
try {
|
||||
// First, get the domain to find the organization
|
||||
const domain = await storage.getDomain(domainId);
|
||||
if (!domain) return;
|
||||
|
||||
// Get webhooks for this organization
|
||||
const webhooks = await storage.getWebhooksByOrganization(domain.organizationId);
|
||||
|
||||
// Filter webhooks that have subscribed to DNS events
|
||||
const dnsWebhooks = webhooks.filter(webhook =>
|
||||
webhook.isActive &&
|
||||
(webhook.events.includes('dns.*') || webhook.events.includes(`dns.${action}`))
|
||||
);
|
||||
|
||||
if (dnsWebhooks.length === 0) return;
|
||||
|
||||
// Create webhook payload
|
||||
const payload = {
|
||||
event: `dns.${action}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: {
|
||||
domain: domain.name,
|
||||
record: recordData,
|
||||
previousRecord: previousData,
|
||||
userId
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger each webhook
|
||||
for (const webhook of dnsWebhooks) {
|
||||
await storage.triggerWebhook(webhook.id, payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error triggering webhooks:", error);
|
||||
// Don't throw - we don't want to interrupt the main operation if webhooks fail
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Setup authentication routes
|
||||
setupAuth(app);
|
||||
@@ -250,6 +297,15 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
req.user?.id
|
||||
);
|
||||
|
||||
// Trigger webhooks
|
||||
await triggerDnsWebhooks(
|
||||
"create",
|
||||
record.domainId,
|
||||
record,
|
||||
previousRecord,
|
||||
req.user?.id || null
|
||||
);
|
||||
|
||||
res.status(201).json(record);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
@@ -298,6 +354,15 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
req.user?.id
|
||||
);
|
||||
|
||||
// Trigger webhooks
|
||||
await triggerDnsWebhooks(
|
||||
"update",
|
||||
updatedRecord.domainId,
|
||||
updatedRecord,
|
||||
previousRecord,
|
||||
req.user?.id || null
|
||||
);
|
||||
|
||||
res.json(updatedRecord);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
@@ -335,6 +400,15 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
req.user?.id
|
||||
);
|
||||
|
||||
// Trigger webhooks
|
||||
await triggerDnsWebhooks(
|
||||
"delete",
|
||||
record.domainId,
|
||||
record,
|
||||
null,
|
||||
req.user?.id || null
|
||||
);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
console.error("Error deleting DNS record:", error);
|
||||
@@ -668,5 +742,174 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Webhooks
|
||||
app.get("/api/webhooks", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
let webhooks;
|
||||
const orgId = req.query.organizationId as string;
|
||||
|
||||
if (orgId) {
|
||||
webhooks = await storage.getWebhooksByOrganization(orgId);
|
||||
} else if (req.user?.role === "admin") {
|
||||
// Admins can see all webhooks
|
||||
const allOrgs = await storage.getOrganizations();
|
||||
webhooks = await Promise.all(
|
||||
allOrgs.map(org => storage.getWebhooksByOrganization(org.id))
|
||||
).then(results => results.flat());
|
||||
} else {
|
||||
// Others can only see webhooks for their organization
|
||||
webhooks = req.user?.organizationId
|
||||
? await storage.getWebhooksByOrganization(req.user.organizationId)
|
||||
: [];
|
||||
}
|
||||
|
||||
res.json(webhooks);
|
||||
} catch (error) {
|
||||
console.error("Error fetching webhooks:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/webhooks/:id", 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" });
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (req.user?.role !== "admin" && webhook.organizationId !== req.user?.organizationId) {
|
||||
return res.status(403).json({ message: "Not authorized to access this webhook" });
|
||||
}
|
||||
|
||||
res.json(webhook);
|
||||
} catch (error) {
|
||||
console.error("Error fetching webhook:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/webhooks", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const validatedData = insertWebhookSchema.parse(req.body);
|
||||
|
||||
// Add the user who created it
|
||||
validatedData.createdBy = req.user!.id;
|
||||
|
||||
// Check authorization for non-admin users
|
||||
if (req.user?.role !== "admin" && validatedData.organizationId !== req.user?.organizationId) {
|
||||
return res.status(403).json({ message: "Not authorized to create webhooks for this organization" });
|
||||
}
|
||||
|
||||
const webhook = await storage.createWebhook(validatedData);
|
||||
res.status(201).json(webhook);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error creating webhook:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/webhooks/:id", 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" });
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (req.user?.role !== "admin" && webhook.organizationId !== req.user?.organizationId) {
|
||||
return res.status(403).json({ message: "Not authorized to modify this webhook" });
|
||||
}
|
||||
|
||||
const validatedData = insertWebhookSchema.partial().parse(req.body);
|
||||
|
||||
// Prevent changing organization for security reasons
|
||||
if (validatedData.organizationId && validatedData.organizationId !== webhook.organizationId) {
|
||||
return res.status(400).json({ message: "Cannot change webhook organization" });
|
||||
}
|
||||
|
||||
const updatedWebhook = await storage.updateWebhook(req.params.id, validatedData);
|
||||
res.json(updatedWebhook);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error updating webhook:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/webhooks/:id", 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" });
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (req.user?.role !== "admin" && webhook.organizationId !== req.user?.organizationId) {
|
||||
return res.status(403).json({ message: "Not authorized to delete this webhook" });
|
||||
}
|
||||
|
||||
const deleted = await storage.deleteWebhook(req.params.id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "Webhook not found" });
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
console.error("Error deleting webhook:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Test webhook endpoint
|
||||
app.post("/api/webhooks/:id/test", 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" });
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (req.user?.role !== "admin" && webhook.organizationId !== req.user?.organizationId) {
|
||||
return res.status(403).json({ message: "Not authorized to test this webhook" });
|
||||
}
|
||||
|
||||
const testPayload = {
|
||||
event: "test",
|
||||
timestamp: new Date().toISOString(),
|
||||
data: {
|
||||
message: "This is a test notification from the DNS Manager",
|
||||
initiatedBy: {
|
||||
userId: req.user!.id,
|
||||
username: req.user!.username
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const success = await storage.triggerWebhook(req.params.id, testPayload);
|
||||
|
||||
if (success) {
|
||||
res.status(200).json({ message: "Test webhook triggered successfully" });
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to trigger webhook" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error testing webhook:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user