mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-07-26 11:38:13 +00:00
40d71909f8
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/d770204f-e03e-46e9-bf65-b96981ac3203.jpg
1557 lines
52 KiB
TypeScript
1557 lines
52 KiB
TypeScript
import type { Express, Request, Response } from "express";
|
|
import { createServer, type Server } from "http";
|
|
import { storage } from "./storage";
|
|
import { db } from "./db";
|
|
import { setupAuth, requireRole } from "./auth";
|
|
import { z } from "zod";
|
|
import { eq } from "drizzle-orm";
|
|
import {
|
|
insertDomainSchema,
|
|
insertDnsRecordSchema,
|
|
insertProviderSchema,
|
|
insertApiTokenSchema,
|
|
insertOrganizationSchema,
|
|
insertWebhookSchema,
|
|
insertDnsMetricSchema,
|
|
insertCustomRoleSchema,
|
|
insertGroupSchema,
|
|
insertGroupMemberSchema,
|
|
recordTypes,
|
|
providerTypes,
|
|
customRoles,
|
|
memberTypes
|
|
} from "@shared/schema";
|
|
import { randomBytes } from "crypto";
|
|
import { getProviderForDomain } from './providers';
|
|
|
|
// Helper function to sync DNS record with provider
|
|
async function syncDnsRecordWithProvider(
|
|
action: string,
|
|
domainId: string,
|
|
recordData: any,
|
|
recordId?: string
|
|
): Promise<string | null> {
|
|
try {
|
|
// Get the domain to find the provider
|
|
const domain = await storage.getDomain(domainId);
|
|
if (!domain || !domain.providerId) {
|
|
console.log(`No domain or provider found for domain ID: ${domainId}`);
|
|
return null;
|
|
}
|
|
|
|
// Get the provider
|
|
const provider = await storage.getProvider(domain.providerId);
|
|
if (!provider) {
|
|
console.log(`Provider not found with ID: ${domain.providerId}`);
|
|
return null;
|
|
}
|
|
|
|
// Initialize the provider
|
|
const dnsProvider = await getProviderForDomain(domain, provider);
|
|
if (!dnsProvider) {
|
|
console.log(`Failed to initialize provider ${provider.name} for domain ${domain.name}`);
|
|
return null;
|
|
}
|
|
|
|
// Get the zone ID for the domain
|
|
const zoneId = await dnsProvider.getZoneIdByName(domain.name);
|
|
if (!zoneId) {
|
|
console.log(`Zone not found for domain ${domain.name}`);
|
|
return null;
|
|
}
|
|
|
|
// Perform the requested action
|
|
let result = null;
|
|
switch (action) {
|
|
case 'create':
|
|
console.log(`Creating record in provider ${provider.name} for domain ${domain.name}`);
|
|
result = await dnsProvider.createRecord(zoneId, recordData);
|
|
if (result && result.id) {
|
|
console.log(`Record created in provider with ID: ${result.id}`);
|
|
return result.id;
|
|
}
|
|
break;
|
|
case 'update':
|
|
if (!recordId) {
|
|
console.log('Record ID is required for update operation');
|
|
return null;
|
|
}
|
|
console.log(`Updating record ${recordId} in provider ${provider.name} for domain ${domain.name}`);
|
|
result = await dnsProvider.updateRecord(zoneId, recordId, recordData);
|
|
if (result) {
|
|
console.log(`Record updated in provider: ${result.id || recordId}`);
|
|
return result.id || recordId;
|
|
}
|
|
break;
|
|
case 'delete':
|
|
if (!recordId) {
|
|
console.log('Record ID is required for delete operation');
|
|
return null;
|
|
}
|
|
console.log(`Deleting record ${recordId} in provider ${provider.name} for domain ${domain.name}`);
|
|
const success = await dnsProvider.deleteRecord(zoneId, recordId);
|
|
if (success) {
|
|
console.log(`Record deleted from provider: ${recordId}`);
|
|
return recordId;
|
|
}
|
|
break;
|
|
default:
|
|
console.log(`Unknown action: ${action}`);
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error(`Error syncing DNS record with provider (${action}):`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
// DNS Records routes
|
|
app.get("/api/dns-records", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const domainId = req.query.domainId as string;
|
|
|
|
if (!domainId) {
|
|
return res.status(400).json({ message: "Domain ID is required" });
|
|
}
|
|
|
|
console.log(`API endpoint: Fetching DNS records for domain: ${domainId}`);
|
|
const records = await storage.getDnsRecordsByDomain(domainId);
|
|
console.log(`API endpoint: Retrieved ${records.length} DNS records from storage`);
|
|
res.json(records);
|
|
} catch (error) {
|
|
console.error("Error fetching DNS records:", error);
|
|
res.status(500).json({
|
|
message: "Failed to fetch DNS records",
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/dns-records/:id", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const recordId = req.params.id;
|
|
const record = await storage.getDnsRecord(recordId);
|
|
|
|
if (!record) {
|
|
return res.status(404).json({ message: "DNS record not found" });
|
|
}
|
|
|
|
res.json(record);
|
|
} catch (error) {
|
|
console.error("Error fetching DNS record:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/dns-records", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
console.log("Creating DNS record with request body:", JSON.stringify(req.body));
|
|
|
|
// Validate the request data
|
|
const validatedData = insertDnsRecordSchema.parse(req.body);
|
|
console.log("Data after validation:", JSON.stringify(validatedData));
|
|
|
|
// Convert "none" to null for providerId if present
|
|
if (validatedData.providerId === "none") {
|
|
validatedData.providerId = null;
|
|
}
|
|
|
|
// Get domain to retrieve providerId if not provided
|
|
if (!validatedData.providerId) {
|
|
const domain = await storage.getDomain(validatedData.domainId);
|
|
if (domain) {
|
|
validatedData.providerId = domain.providerId;
|
|
console.log(`Setting providerId from domain: ${domain.providerId}`);
|
|
} else {
|
|
console.log(`Domain not found: ${validatedData.domainId}`);
|
|
}
|
|
}
|
|
|
|
// Make sure content is never undefined or null for non-AUTO records
|
|
if (!validatedData.isAutoIP && (!validatedData.content || validatedData.content.trim() === '')) {
|
|
validatedData.content = '';
|
|
console.log("Setting empty content for non-AUTO record");
|
|
}
|
|
|
|
try {
|
|
console.log("Calling storage.createDnsRecord with:", JSON.stringify(validatedData));
|
|
const record = await storage.createDnsRecord(validatedData);
|
|
console.log("DNS record created successfully:", JSON.stringify(record));
|
|
|
|
// Track history
|
|
await storage.addDnsHistory(
|
|
record.id,
|
|
"create",
|
|
undefined,
|
|
JSON.stringify(record),
|
|
req.user?.id
|
|
);
|
|
|
|
// Trigger webhooks
|
|
await triggerDnsWebhooks("create", record.domainId, record, null, req.user?.id);
|
|
|
|
res.status(201).json(record);
|
|
} catch (error) {
|
|
console.error("Error creating DNS record in storage:", error);
|
|
res.status(500).json({
|
|
message: "Failed to create DNS record",
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
console.error("Validation error:", error.errors);
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Unexpected error creating DNS record:", error);
|
|
res.status(500).json({
|
|
message: "Internal server error",
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
app.put("/api/dns-records/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
console.log("Processing DNS record update for id:", req.params.id);
|
|
console.log("Update payload:", JSON.stringify(req.body));
|
|
|
|
const recordId = req.params.id;
|
|
const validatedData = insertDnsRecordSchema.partial().parse(req.body);
|
|
|
|
// Convert "none" to null for providerId if present
|
|
if (validatedData.providerId === "none") {
|
|
validatedData.providerId = null;
|
|
}
|
|
|
|
// Get current record for history
|
|
const currentRecord = await storage.getDnsRecord(recordId);
|
|
if (!currentRecord) {
|
|
console.log(`DNS record not found with ID: ${recordId}`);
|
|
return res.status(404).json({ message: "DNS record not found" });
|
|
}
|
|
|
|
console.log(`Found existing record:`, JSON.stringify(currentRecord));
|
|
|
|
// Get domain to retrieve providerId if needed
|
|
if (!validatedData.providerId) {
|
|
const domain = await storage.getDomain(currentRecord.domainId);
|
|
if (domain) {
|
|
console.log(`Using domain provider ID from domain:`, domain.providerId);
|
|
validatedData.providerId = domain.providerId;
|
|
}
|
|
}
|
|
|
|
// Log what we're about to update
|
|
console.log(`Updating record with data:`, JSON.stringify(validatedData));
|
|
|
|
// Update the record
|
|
const record = await storage.updateDnsRecord(recordId, validatedData);
|
|
|
|
if (!record) {
|
|
console.log(`Update failed - record not found or update failed`);
|
|
return res.status(404).json({ message: "DNS record not found or update failed" });
|
|
}
|
|
|
|
console.log(`Record updated successfully:`, JSON.stringify(record));
|
|
|
|
try {
|
|
// Track history
|
|
await storage.addDnsHistory(
|
|
recordId,
|
|
"update",
|
|
JSON.stringify(currentRecord),
|
|
JSON.stringify(record),
|
|
req.user?.id
|
|
);
|
|
|
|
// Trigger webhooks
|
|
await triggerDnsWebhooks("update", record.domainId, record, currentRecord, req.user?.id);
|
|
} catch (historyError) {
|
|
// Don't fail the request if history tracking fails
|
|
console.error("Error tracking history:", historyError);
|
|
}
|
|
|
|
res.json(record);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
console.error("Validation error:", error.errors);
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error updating DNS record:", error);
|
|
res.status(500).json({
|
|
message: "Failed to update DNS record",
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
app.delete("/api/dns-records/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const recordId = req.params.id;
|
|
console.log(`Processing DNS record deletion for ID: ${recordId}`);
|
|
|
|
// Get current record for history and webhooks
|
|
const currentRecord = await storage.getDnsRecord(recordId);
|
|
if (!currentRecord) {
|
|
console.log(`DNS record not found for deletion with ID: ${recordId}`);
|
|
return res.status(404).json({ message: "DNS record not found" });
|
|
}
|
|
|
|
console.log(`Found record to delete:`, JSON.stringify(currentRecord));
|
|
|
|
// Perform the deletion
|
|
const success = await storage.deleteDnsRecord(recordId);
|
|
|
|
if (!success) {
|
|
console.log(`Delete operation failed for record ID: ${recordId}`);
|
|
return res.status(404).json({ message: "DNS record not found or could not be deleted" });
|
|
}
|
|
|
|
console.log(`Successfully deleted record with ID: ${recordId}`);
|
|
|
|
try {
|
|
// Track history
|
|
await storage.addDnsHistory(
|
|
recordId,
|
|
"delete",
|
|
JSON.stringify(currentRecord),
|
|
undefined,
|
|
req.user?.id
|
|
);
|
|
|
|
// Trigger webhooks
|
|
await triggerDnsWebhooks("delete", currentRecord.domainId, null, currentRecord, req.user?.id);
|
|
|
|
console.log(`History and webhooks processed for deleted record ${recordId}`);
|
|
} catch (historyError) {
|
|
// Don't fail the request if history tracking fails
|
|
console.error("Error tracking history for deleted record:", historyError);
|
|
}
|
|
|
|
// Return success
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error deleting DNS record:", error);
|
|
res.status(500).json({
|
|
message: "Failed to delete DNS record",
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
}
|
|
});
|
|
|
|
const httpServer = createServer(app);
|
|
|
|
// Organizations
|
|
app.get("/api/organizations", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const organizations = await storage.getOrganizations();
|
|
res.json(organizations);
|
|
} catch (error) {
|
|
console.error("Error fetching organizations:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/organizations/:id", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const organization = await storage.getOrganization(id);
|
|
|
|
if (!organization) {
|
|
return res.status(404).json({ message: "Organization not found" });
|
|
}
|
|
|
|
res.json(organization);
|
|
} catch (error) {
|
|
console.error("Error fetching organization:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/organizations", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const validatedData = insertOrganizationSchema.parse(req.body);
|
|
const organization = await storage.createOrganization(validatedData);
|
|
res.status(201).json(organization);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error creating organization:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.put("/api/organizations/:id", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const validatedData = insertOrganizationSchema.partial().parse(req.body);
|
|
|
|
const updatedOrganization = await storage.updateOrganization(id, validatedData);
|
|
|
|
if (!updatedOrganization) {
|
|
return res.status(404).json({ message: "Organization not found" });
|
|
}
|
|
|
|
res.json(updatedOrganization);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error updating organization:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.delete("/api/organizations/:id", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const deleted = await storage.deleteOrganization(id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ message: "Organization not found" });
|
|
}
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error deleting organization:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Domains
|
|
app.get("/api/domains", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
let domains;
|
|
const orgId = req.query.organizationId ? parseInt(req.query.organizationId as string) : undefined;
|
|
|
|
if (orgId) {
|
|
domains = await storage.getDomainsByOrganization(orgId);
|
|
} else {
|
|
domains = await storage.getAllDomains();
|
|
}
|
|
|
|
res.json(domains);
|
|
} catch (error) {
|
|
console.error("Error fetching domains:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/domains/:id", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
// Use the id as a string directly without parsing as integer
|
|
const id = req.params.id;
|
|
const domain = await storage.getDomain(id);
|
|
|
|
if (!domain) {
|
|
return res.status(404).json({ message: "Domain not found" });
|
|
}
|
|
|
|
res.json(domain);
|
|
} catch (error) {
|
|
console.error("Error fetching domain:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/domains", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
// Create a custom validation schema for the API that makes providerId optional
|
|
const domainSchema = z.object({
|
|
name: z.string(),
|
|
organizationId: z.string().uuid(),
|
|
providerId: z.string().uuid().optional(),
|
|
isActive: z.boolean().optional().default(true)
|
|
});
|
|
|
|
const validatedData = domainSchema.parse(req.body);
|
|
const domain = await storage.createDomain(validatedData);
|
|
res.status(201).json(domain);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error creating domain:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.put("/api/domains/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
// Use id as string without parsing to int
|
|
const id = req.params.id;
|
|
|
|
// Create custom validation schema with optional providerId
|
|
const domainUpdateSchema = z.object({
|
|
name: z.string().optional(),
|
|
organizationId: z.string().uuid().optional(),
|
|
providerId: z.string().uuid().optional().nullable(),
|
|
isActive: z.boolean().optional()
|
|
});
|
|
|
|
const validatedData = domainUpdateSchema.parse(req.body);
|
|
|
|
const updatedDomain = await storage.updateDomain(id, validatedData);
|
|
|
|
if (!updatedDomain) {
|
|
return res.status(404).json({ message: "Domain not found" });
|
|
}
|
|
|
|
res.json(updatedDomain);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error updating domain:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.delete("/api/domains/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
// Use id as string without parsing
|
|
const id = req.params.id;
|
|
const deleted = await storage.deleteDomain(id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ message: "Domain not found" });
|
|
}
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error deleting domain:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// DNS Records API routes are already defined above
|
|
|
|
// Providers
|
|
app.get("/api/providers", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const providers = await storage.getProviders();
|
|
|
|
// Mask credentials in response
|
|
const maskedProviders = providers.map(provider => {
|
|
if (provider.credentials) {
|
|
return {
|
|
...provider,
|
|
credentials: { masked: true }
|
|
};
|
|
}
|
|
return provider;
|
|
});
|
|
|
|
res.json(maskedProviders);
|
|
} catch (error) {
|
|
console.error("Error fetching providers:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/providers/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const provider = await storage.getProvider(id);
|
|
|
|
if (!provider) {
|
|
return res.status(404).json({ message: "Provider not found" });
|
|
}
|
|
|
|
// Mask credentials in response
|
|
const maskedProvider = {
|
|
...provider,
|
|
credentials: provider.credentials ? { masked: true } : null
|
|
};
|
|
|
|
res.json(maskedProvider);
|
|
} catch (error) {
|
|
console.error("Error fetching provider:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/providers", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
// Validate provider type
|
|
const providerTypeValidator = z.enum(providerTypes);
|
|
|
|
// Extend schema with validation
|
|
const schema = insertProviderSchema.extend({
|
|
type: providerTypeValidator
|
|
});
|
|
|
|
const validatedData = schema.parse(req.body);
|
|
const provider = await storage.createProvider(validatedData);
|
|
|
|
// Mask credentials in response
|
|
const maskedProvider = {
|
|
...provider,
|
|
credentials: provider.credentials ? { masked: true } : null
|
|
};
|
|
|
|
res.status(201).json(maskedProvider);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error creating provider:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.put("/api/providers/:id", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
|
|
// Validate provider type
|
|
const providerTypeValidator = z.enum(providerTypes);
|
|
|
|
// Extend schema with validation
|
|
const schema = insertProviderSchema.partial().extend({
|
|
type: providerTypeValidator.optional()
|
|
});
|
|
|
|
const validatedData = schema.parse(req.body);
|
|
|
|
const updatedProvider = await storage.updateProvider(id, validatedData);
|
|
|
|
if (!updatedProvider) {
|
|
return res.status(404).json({ message: "Provider not found" });
|
|
}
|
|
|
|
// Mask credentials in response
|
|
const maskedProvider = {
|
|
...updatedProvider,
|
|
credentials: updatedProvider.credentials ? { masked: true } : null
|
|
};
|
|
|
|
res.json(maskedProvider);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error updating provider:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.delete("/api/providers/:id", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
const deleted = await storage.deleteProvider(id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ message: "Provider not found" });
|
|
}
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error deleting provider:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Groups
|
|
app.get("/api/groups", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const groups = await storage.getGroups();
|
|
res.json(groups);
|
|
} catch (error) {
|
|
console.error("Error fetching groups:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/groups/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const group = await storage.getGroup(id);
|
|
|
|
if (!group) {
|
|
return res.status(404).json({ message: "Group not found" });
|
|
}
|
|
|
|
res.json(group);
|
|
} catch (error) {
|
|
console.error("Error fetching group:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/groups/:id/members", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const members = await storage.getGroupMembers(id);
|
|
res.json(members);
|
|
} catch (error) {
|
|
console.error("Error fetching group members:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/groups", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const validatedData = insertGroupSchema.parse({
|
|
...req.body,
|
|
createdBy: req.user?.id
|
|
});
|
|
const group = await storage.createGroup(validatedData);
|
|
res.status(201).json(group);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error creating group:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.put("/api/groups/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const validatedData = insertGroupSchema.partial().parse(req.body);
|
|
|
|
const updatedGroup = await storage.updateGroup(id, validatedData);
|
|
|
|
if (!updatedGroup) {
|
|
return res.status(404).json({ message: "Group not found" });
|
|
}
|
|
|
|
res.json(updatedGroup);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error updating group:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.delete("/api/groups/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const deleted = await storage.deleteGroup(id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ message: "Group not found" });
|
|
}
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error deleting group:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Group Members
|
|
app.post("/api/group-members", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
// Validate member type
|
|
const memberTypeValidator = z.enum(memberTypes);
|
|
|
|
// Extend schema with validation
|
|
const schema = insertGroupMemberSchema.extend({
|
|
memberType: memberTypeValidator
|
|
});
|
|
|
|
const validatedData = schema.parse({
|
|
...req.body,
|
|
addedBy: req.user?.id
|
|
});
|
|
|
|
const member = await storage.addGroupMember(validatedData);
|
|
res.status(201).json(member);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error adding group member:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.delete("/api/group-members/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = req.params.id;
|
|
const deleted = await storage.removeGroupMember(id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ message: "Group member not found" });
|
|
}
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error removing group member:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// API Tokens
|
|
app.get("/api/api-tokens", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
let tokens;
|
|
const orgId = req.query.organizationId ? parseInt(req.query.organizationId as string) : undefined;
|
|
|
|
if (orgId) {
|
|
tokens = await storage.getApiTokensByOrganization(orgId);
|
|
} else if (req.user?.role === "admin") {
|
|
// Admins can see all tokens
|
|
const allOrgs = await storage.getOrganizations();
|
|
tokens = await Promise.all(
|
|
allOrgs.map(org => storage.getApiTokensByOrganization(org.id))
|
|
).then(results => results.flat());
|
|
} else {
|
|
// Others can only see tokens for their organization
|
|
tokens = req.user?.organizationId
|
|
? await storage.getApiTokensByOrganization(req.user.organizationId)
|
|
: [];
|
|
}
|
|
|
|
// Mask token values
|
|
const maskedTokens = tokens.map(token => ({
|
|
...token,
|
|
token: token.token.substring(0, 8) + '...'
|
|
}));
|
|
|
|
res.json(maskedTokens);
|
|
} catch (error) {
|
|
console.error("Error fetching API tokens:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/api-tokens", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
// Generate a random token
|
|
const tokenValue = randomBytes(32).toString('hex');
|
|
|
|
// Validate permissions
|
|
const permissionsValidator = z.array(z.enum(["admin", "manager", "user", "readonly"]));
|
|
|
|
// Create schema with additional validation
|
|
const schema = insertApiTokenSchema
|
|
.omit({ token: true })
|
|
.extend({
|
|
permissions: permissionsValidator,
|
|
name: z.string().min(1)
|
|
});
|
|
|
|
const validatedData = schema.parse(req.body);
|
|
|
|
// Set token value and created by
|
|
const tokenData = {
|
|
...validatedData,
|
|
token: tokenValue,
|
|
createdBy: req.user?.id
|
|
};
|
|
|
|
const token = await storage.createApiToken(tokenData);
|
|
|
|
// Return the full token only on creation
|
|
res.status(201).json(token);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error creating API token:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.put("/api/api-tokens/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
|
|
// Get the token
|
|
const token = await storage.getApiToken(id);
|
|
|
|
if (!token) {
|
|
return res.status(404).json({ message: "API token not found" });
|
|
}
|
|
|
|
// Verify permission: only admin can modify any token, managers can only modify their org's tokens
|
|
if (req.user?.role !== "admin" && token.organizationId !== req.user?.organizationId) {
|
|
return res.status(403).json({ message: "Insufficient permissions" });
|
|
}
|
|
|
|
// Validate permissions
|
|
const permissionsValidator = z.array(z.enum(["admin", "manager", "user", "readonly"])).optional();
|
|
|
|
// Create schema with additional validation
|
|
const schema = insertApiTokenSchema
|
|
.omit({ token: true })
|
|
.partial()
|
|
.extend({
|
|
permissions: permissionsValidator,
|
|
name: z.string().min(1).optional()
|
|
});
|
|
|
|
const validatedData = schema.parse(req.body);
|
|
|
|
const updatedToken = await storage.updateApiToken(id, validatedData);
|
|
|
|
if (!updatedToken) {
|
|
return res.status(404).json({ message: "API token not found" });
|
|
}
|
|
|
|
// Mask token in response
|
|
const maskedToken = {
|
|
...updatedToken,
|
|
token: updatedToken.token.substring(0, 8) + '...'
|
|
};
|
|
|
|
res.json(maskedToken);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error updating API token:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
app.delete("/api/api-tokens/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const id = parseInt(req.params.id);
|
|
|
|
// Get the token
|
|
const token = await storage.getApiToken(id);
|
|
|
|
if (!token) {
|
|
return res.status(404).json({ message: "API token not found" });
|
|
}
|
|
|
|
// Verify permission: only admin can delete any token, managers can only delete their org's tokens
|
|
if (req.user?.role !== "admin" && token.organizationId !== req.user?.organizationId) {
|
|
return res.status(403).json({ message: "Insufficient permissions" });
|
|
}
|
|
|
|
const deleted = await storage.deleteApiToken(id);
|
|
|
|
if (!deleted) {
|
|
return res.status(404).json({ message: "API token not found" });
|
|
}
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error deleting API token:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// DNS History
|
|
app.get("/api/dns-history/record/:recordId", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const recordId = parseInt(req.params.recordId);
|
|
const history = await storage.getDnsHistoryByRecord(recordId);
|
|
res.json(history);
|
|
} catch (error) {
|
|
console.error("Error fetching DNS history:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/dns-history/domain/:domainId", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const domainId = parseInt(req.params.domainId);
|
|
const history = await storage.getDnsHistoryByDomain(domainId);
|
|
res.json(history);
|
|
} catch (error) {
|
|
console.error("Error fetching DNS history:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// User Management (admin only)
|
|
app.get("/api/users", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
// This would use a real database query to get all users
|
|
// For the in-memory storage, we'll return the currently stored users
|
|
const users = Array.from(storage.usersMap ? storage.usersMap.values() : []);
|
|
|
|
// Remove passwords from response
|
|
const sanitizedUsers = users.map(user => {
|
|
const { password, ...userWithoutPassword } = user;
|
|
return userWithoutPassword;
|
|
});
|
|
|
|
res.json(sanitizedUsers);
|
|
} catch (error) {
|
|
console.error("Error fetching users:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// DNS Metrics
|
|
app.get("/api/dns-metrics", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const metricType = req.query.type ? req.query.type as string : undefined;
|
|
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined;
|
|
const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
|
|
|
|
const metrics = await storage.getDnsMetricsByType(
|
|
metricType || "all",
|
|
startDate,
|
|
endDate
|
|
);
|
|
|
|
res.json(metrics);
|
|
} catch (error) {
|
|
console.error("Error fetching DNS metrics:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/dns-metrics/domain/:domainId", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const domainId = req.params.domainId;
|
|
const metricType = req.query.type ? req.query.type as string : undefined;
|
|
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined;
|
|
const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
|
|
|
|
const metrics = await storage.getDnsMetricsByDomain(
|
|
domainId,
|
|
metricType,
|
|
startDate,
|
|
endDate
|
|
);
|
|
|
|
res.json(metrics);
|
|
} catch (error) {
|
|
console.error("Error fetching domain DNS metrics:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.get("/api/dns-metrics/record/:recordId", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
|
try {
|
|
const recordId = req.params.recordId;
|
|
const metricType = req.query.type ? req.query.type as string : undefined;
|
|
const startDate = req.query.startDate ? new Date(req.query.startDate as string) : undefined;
|
|
const endDate = req.query.endDate ? new Date(req.query.endDate as string) : undefined;
|
|
|
|
const metrics = await storage.getDnsMetricsByRecord(
|
|
recordId,
|
|
metricType,
|
|
startDate,
|
|
endDate
|
|
);
|
|
|
|
res.json(metrics);
|
|
} catch (error) {
|
|
console.error("Error fetching record DNS metrics:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/dns-metrics", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
const validatedData = insertDnsMetricSchema.parse(req.body);
|
|
const metric = await storage.addDnsMetric(validatedData);
|
|
res.status(201).json(metric);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error creating DNS metric:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
// Webhooks
|
|
app.get("/api/webhooks", async (req, res) => {
|
|
try {
|
|
// Check if user is authenticated
|
|
if (!req.isAuthenticated()) {
|
|
return res.status(401).json({ message: "Not authenticated" });
|
|
}
|
|
|
|
// Check if user has appropriate role (admin or manager)
|
|
const userRole = req.user?.role;
|
|
if (userRole !== 'admin' && userRole !== 'manager') {
|
|
return res.status(403).json({ message: "Insufficient permissions" });
|
|
}
|
|
|
|
let webhooks;
|
|
const orgId = req.query.organizationId as string;
|
|
|
|
if (orgId) {
|
|
// For specific organization, check if user belongs to that org
|
|
if (userRole !== "admin" && req.user?.organizationId !== orgId) {
|
|
return res.status(403).json({ message: "Insufficient permissions" });
|
|
}
|
|
webhooks = await storage.getWebhooksByOrganization(orgId);
|
|
} else if (userRole === "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 {
|
|
// Merge request body with user ID before validation
|
|
const requestData = {
|
|
...req.body,
|
|
createdBy: req.user?.id // Add the user who created it
|
|
};
|
|
|
|
// Then validate the complete data
|
|
const validatedData = insertWebhookSchema.parse(requestData);
|
|
|
|
// 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) {
|
|
console.error("Validation error:", error.errors);
|
|
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" });
|
|
}
|
|
});
|
|
|
|
// Webhook Delivery Log endpoints
|
|
|
|
// Get all delivery logs for a webhook
|
|
app.get("/api/webhooks/:id/logs", requireRole(["admin", "manager"]), async (req, res) => {
|
|
try {
|
|
// First check if the webhook exists
|
|
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" });
|
|
}
|
|
|
|
try {
|
|
// Try to get logs with proper error handling
|
|
const logs = await storage.getWebhookDeliveryLogsByWebhook(req.params.id);
|
|
res.json(logs);
|
|
} catch (logError) {
|
|
// If there's a database schema issue, return an empty array instead of crashing
|
|
console.error("Error fetching webhook logs:", logError);
|
|
res.json([]);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in webhook logs endpoint:", 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" });
|
|
}
|
|
});
|
|
|
|
// Retry a failed webhook delivery
|
|
app.post("/api/webhook-logs/:id/retry", 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 and retry
|
|
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 retry this webhook" });
|
|
}
|
|
|
|
// Only retry failed deliveries
|
|
if (log.status) {
|
|
return res.status(400).json({ message: "Cannot retry successful webhook delivery" });
|
|
}
|
|
|
|
// Retry the webhook with the original payload but increment retry count
|
|
const retrySuccess = await storage.triggerWebhook(
|
|
webhook.id,
|
|
log.payload,
|
|
log.retryCount + 1
|
|
);
|
|
|
|
if (retrySuccess) {
|
|
res.status(200).json({ message: "Webhook delivery retried successfully" });
|
|
} else {
|
|
res.status(500).json({ message: "Failed to retry webhook delivery" });
|
|
}
|
|
} catch (error) {
|
|
console.error("Error retrying webhook delivery:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Custom Roles endpoints
|
|
|
|
// Get all custom roles
|
|
app.get("/api/roles/custom", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
// Only admins can view custom roles
|
|
const customRoles = await db.query.customRoles.findMany({
|
|
orderBy: (roles, { desc }) => [desc(roles.createdAt)],
|
|
});
|
|
|
|
res.json(customRoles);
|
|
} catch (error) {
|
|
console.error("Error fetching custom roles:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Get a single custom role by ID
|
|
app.get("/api/roles/custom/:id", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const role = await db.query.customRoles.findFirst({
|
|
where: eq(customRoles.id, req.params.id),
|
|
});
|
|
|
|
if (!role) {
|
|
return res.status(404).json({ message: "Role not found" });
|
|
}
|
|
|
|
res.json(role);
|
|
} catch (error) {
|
|
console.error("Error fetching custom role:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Create a new custom role
|
|
app.post("/api/roles/custom", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const validatedData = insertCustomRoleSchema.parse(req.body);
|
|
|
|
// Check if a role with this name already exists
|
|
const existingRole = await db.query.customRoles.findFirst({
|
|
where: eq(customRoles.name, validatedData.name),
|
|
});
|
|
|
|
if (existingRole) {
|
|
return res.status(400).json({ message: "A role with this name already exists" });
|
|
}
|
|
|
|
const [newRole] = await db.insert(customRoles).values(validatedData).returning();
|
|
|
|
res.status(201).json(newRole);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error creating custom role:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update a custom role
|
|
app.put("/api/roles/custom/:id", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const roleId = req.params.id;
|
|
|
|
// Check if the role exists
|
|
const existingRole = await db.query.customRoles.findFirst({
|
|
where: eq(customRoles.id, roleId),
|
|
});
|
|
|
|
if (!existingRole) {
|
|
return res.status(404).json({ message: "Role not found" });
|
|
}
|
|
|
|
// Validate the request data
|
|
const validatedData = insertCustomRoleSchema.partial().parse(req.body);
|
|
|
|
// If name is being updated, check for duplicates
|
|
if (validatedData.name && validatedData.name !== existingRole.name) {
|
|
const duplicateName = await db.query.customRoles.findFirst({
|
|
where: eq(customRoles.name, validatedData.name),
|
|
});
|
|
|
|
if (duplicateName) {
|
|
return res.status(400).json({ message: "A role with this name already exists" });
|
|
}
|
|
}
|
|
|
|
// Update the role
|
|
const [updatedRole] = await db
|
|
.update(customRoles)
|
|
.set(validatedData)
|
|
.where(eq(customRoles.id, roleId))
|
|
.returning();
|
|
|
|
res.json(updatedRole);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
res.status(400).json({ message: "Validation error", errors: error.errors });
|
|
} else {
|
|
console.error("Error updating custom role:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
}
|
|
});
|
|
|
|
// Delete a custom role
|
|
app.delete("/api/roles/custom/:id", requireRole(["admin"]), async (req, res) => {
|
|
try {
|
|
const roleId = req.params.id;
|
|
|
|
// Check if the role exists
|
|
const existingRole = await db.query.customRoles.findFirst({
|
|
where: eq(customRoles.id, roleId),
|
|
});
|
|
|
|
if (!existingRole) {
|
|
return res.status(404).json({ message: "Role not found" });
|
|
}
|
|
|
|
// Delete the role
|
|
await db.delete(customRoles).where(eq(customRoles.id, roleId));
|
|
|
|
res.status(204).end();
|
|
} catch (error) {
|
|
console.error("Error deleting custom role:", error);
|
|
res.status(500).json({ message: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
return httpServer;
|
|
}
|