Files
DynamoDNS/server/routes.ts
T
alphaeusmote 82652c971f Update API to improve customer management and access control.
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/ed62e9a4-d893-4126-a108-546d113b5c99.jpg
2025-04-12 00:43:52 +00:00

1913 lines
68 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 { getCurrentIpAddress, getCurrentIpv6Address } from "./utils/ip-utils";
import {
insertDomainSchema,
insertDnsRecordSchema,
insertProviderSchema,
insertApiTokenSchema,
insertCustomerSchema,
insertWebhookSchema,
insertDnsMetricSchema,
insertCustomRoleSchema,
insertCustomerUserAssignmentSchema,
insertDomainCredentialsSchema,
insertApiTokenCustomerAccessSchema,
recordTypes,
providerTypes,
customRoles,
systemRoles
} 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 customer
const webhooks = await storage.getWebhooksByCustomer(domain.customerId);
// 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> {
// Create HTTP server
const httpServer = createServer(app);
// Add public IP endpoint for the frontend
app.get('/api/public-ip', async (req, res) => {
try {
console.log('Fetching public IP addresses');
// Get IPv4 address using our utility function
const ipv4 = await getCurrentIpAddress();
console.log(`Resolved IPv4: ${ipv4}`);
// Try to get IPv6 as well
const ipv6 = await getCurrentIpv6Address();
console.log(`Resolved IPv6: ${ipv6}`);
res.json({ ipv4, ipv6 });
} catch (error) {
console.error('Error getting public IP:', error);
res.status(500).json({ error: 'Failed to get public IP address' });
}
});
return httpServer;
// 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`);
// Process records to show current IP for auto IP records
const processedRecords = await Promise.all(records.map(async (record) => {
// For A records with auto IP enabled, fetch the current IP
if (record.isAutoIP && (record.type === 'A' || record.type === 'AAAA')) {
console.log(`Processing AutoIP record: ${record.id}, type: ${record.type}, name: ${record.name}`);
try {
// Get the appropriate IP address based on record type
let currentIp = null;
if (record.type === 'A') {
console.log(`Fetching IPv4 address for record ${record.id}`);
currentIp = await getCurrentIpAddress();
console.log(`Fetched IPv4 address: ${currentIp}`);
} else if (record.type === 'AAAA') {
console.log(`Fetching IPv6 address for record ${record.id}`);
currentIp = await getCurrentIpv6Address();
console.log(`Fetched IPv6 address: ${currentIp}`);
}
if (currentIp) {
console.log(`Found current IP for auto record ${record.id}: ${currentIp}`);
// Create a new object to avoid modifying the original record
return {
...record,
// Store the actual IP in the response for display purposes
currentIp: currentIp,
// Keep content as is - the UI will show currentIp if isAutoIP is true
};
}
} catch (ipError) {
console.error(`Error getting current IP for record ${record.id}:`, ipError);
}
}
// Return the record unmodified if it's not auto IP or if there was an error
return record;
}));
res.json(processedRecords);
} 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;
console.log(`Fetching DNS record with ID: ${recordId}`);
const record = await storage.getDnsRecord(recordId);
if (!record) {
console.log(`DNS record not found with ID: ${recordId}`);
return res.status(404).json({ message: "DNS record not found" });
}
console.log(`Found DNS record: ${JSON.stringify(record)}`);
// For A/AAAA records with auto IP enabled, fetch the current IP address
if (record.isAutoIP && (record.type === 'A' || record.type === 'AAAA')) {
console.log(`Processing AutoIP record: ${record.id}, type: ${record.type}, name: ${record.name}`);
try {
// Get the appropriate IP address based on record type
let currentIp = null;
if (record.type === 'A') {
console.log(`Fetching IPv4 address for record ${record.id}`);
currentIp = await getCurrentIpAddress();
console.log(`Fetched IPv4 address: ${currentIp}`);
} else if (record.type === 'AAAA') {
console.log(`Fetching IPv6 address for record ${record.id}`);
currentIp = await getCurrentIpv6Address();
console.log(`Fetched IPv6 address: ${currentIp}`);
}
if (currentIp) {
console.log(`Found current IP for auto record ${record.id}: ${currentIp}`);
// Create a new object to avoid modifying the original record
return res.json({
...record,
// Store the actual IP in the response for display purposes
currentIp: currentIp
});
}
} catch (ipError) {
console.error(`Error getting current IP for record ${record.id}:`, ipError);
}
}
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
);
// Sync with provider if applicable
try {
const providerRecordId = await syncDnsRecordWithProvider('create', record.domainId, record);
if (providerRecordId) {
console.log(`Record synced with provider, got provider record ID: ${providerRecordId}`);
// Update the record with the provider record ID
await storage.updateDnsRecord(record.id, {
providerRecordId
});
}
} catch (providerError) {
console.error("Error syncing with provider (continuing):", providerError);
// We don't fail the request if provider sync fails
}
// 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)
});
}
}
});
// PATCH endpoint for simple updates (like toggling active status)
app.patch("/api/dns-records/:id", requireRole(["admin", "manager"]), async (req, res) => {
try {
console.log("Processing DNS record PATCH for id:", req.params.id);
console.log("PATCH payload:", JSON.stringify(req.body));
const recordId = req.params.id;
const validatedData = insertDnsRecordSchema.partial().parse(req.body);
// 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));
// For PATCH operations, we only update the specific fields provided
console.log(`Applying PATCH with data:`, JSON.stringify(validatedData));
// Update the record
const record = await storage.updateDnsRecord(recordId, validatedData);
if (!record) {
console.log(`PATCH failed - record not found or update failed`);
return res.status(404).json({ message: "DNS record not found or update failed" });
}
console.log(`Record patched successfully:`, JSON.stringify(record));
// Track history
await storage.addDnsHistory(
recordId,
"patch",
JSON.stringify(currentRecord),
JSON.stringify(record),
req.user?.id
);
// Sync with provider if applicable, but only if we're changing isActive status
if (validatedData.isActive !== undefined) {
try {
const providerRecordId = await syncDnsRecordWithProvider(
'update',
record.domainId,
record,
record.providerRecordId || undefined
);
if (providerRecordId && providerRecordId !== record.providerRecordId) {
console.log(`Record synced with provider, updating provider record ID: ${providerRecordId}`);
// Update the record with the provider record ID
await storage.updateDnsRecord(record.id, { providerRecordId });
}
} catch (syncError) {
console.error("Failed to sync with provider:", syncError);
// Don't fail the request if sync fails, just log the error
}
}
return res.json(record);
} catch (error) {
console.error("Error in PATCH DNS record:", error);
return res.status(500).json({ message: "Failed to update DNS record", error: 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;
}
}
// Handle the proxied field separately for Cloudflare records
if (validatedData.proxied !== undefined) {
console.log(`Handling proxied field with value:`, validatedData.proxied);
}
// 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
);
// Sync with provider if applicable
try {
const providerRecordId = await syncDnsRecordWithProvider(
'update',
record.domainId,
record,
record.providerRecordId || undefined
);
if (providerRecordId && providerRecordId !== record.providerRecordId) {
console.log(`Record synced with provider, updating provider record ID: ${providerRecordId}`);
// Update the record with the provider record ID
await storage.updateDnsRecord(record.id, {
providerRecordId
});
}
} catch (providerError) {
console.error("Error syncing with provider (continuing):", providerError);
// We don't fail the request if provider sync fails
}
// 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
);
// Sync with provider if applicable
try {
if (currentRecord.providerRecordId) {
console.log(`Deleting record with provider record ID: ${currentRecord.providerRecordId}`);
await syncDnsRecordWithProvider('delete', currentRecord.domainId, null, currentRecord.providerRecordId);
console.log(`Record deleted from provider successfully`);
} else {
console.log(`No provider record ID found for record ${recordId}, skipping provider sync`);
}
} catch (providerError) {
console.error("Error syncing with provider (continuing):", providerError);
// We don't fail the request if provider sync fails
}
// 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);
// Customers (previously organizations)
app.get("/api/customers", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
try {
const customers = await storage.getCustomers();
res.json(customers);
} catch (error) {
console.error("Error fetching customers:", error);
res.status(500).json({ message: "Internal server error" });
}
});
app.get("/api/customers/:id", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
try {
const customer = await storage.getCustomer(req.params.id);
if (!customer) {
return res.status(404).json({ message: "Customer not found" });
}
res.json(customer);
} catch (error) {
console.error("Error fetching customer:", error);
res.status(500).json({ message: "Internal server error" });
}
});
app.post("/api/customers", requireRole(["admin"]), async (req, res) => {
try {
const validatedData = insertCustomerSchema.parse(req.body);
const customer = await storage.createCustomer(validatedData);
res.status(201).json(customer);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ message: "Validation error", errors: error.errors });
} else {
console.error("Error creating customer:", error);
res.status(500).json({ message: "Internal server error" });
}
}
});
app.put("/api/customers/:id", requireRole(["admin"]), async (req, res) => {
try {
const validatedData = insertCustomerSchema.partial().parse(req.body);
const updatedCustomer = await storage.updateCustomer(req.params.id, validatedData);
if (!updatedCustomer) {
return res.status(404).json({ message: "Customer not found" });
}
res.json(updatedCustomer);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ message: "Validation error", errors: error.errors });
} else {
console.error("Error updating customer:", error);
res.status(500).json({ message: "Internal server error" });
}
}
});
// Legacy endpoint for backward compatibility
app.get("/api/organizations", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
try {
const customers = await storage.getCustomers();
res.json(customers);
} catch (error) {
console.error("Error fetching customers:", error);
res.status(500).json({ message: "Internal server error" });
}
});
// Get all customers
app.get("/api/customers", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
try {
const customers = await storage.getCustomers();
res.json(customers);
} catch (error) {
console.error("Error fetching customers:", error);
res.status(500).json({ message: "Internal server error" });
}
});
// Delete a customer
app.delete("/api/customers/:id", requireRole(["admin"]), async (req, res) => {
try {
const deleted = await storage.deleteCustomer(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Customer not found" });
}
res.status(204).end();
} catch (error) {
console.error("Error deleting customer:", 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 customerId = req.query.customerId as string;
if (customerId) {
domains = await storage.getDomainsByCustomer(customerId);
} 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(),
customerId: 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(),
customerId: 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 functionality has been removed
// Group Members endpoints have been removed
// API Tokens
app.get("/api/api-tokens", requireRole(["admin", "manager"]), async (req, res) => {
try {
let tokens = [];
const customerId = req.query.customerId ? req.query.customerId as string : undefined;
if (customerId) {
// Get tokens for specific customer
tokens = await storage.getApiTokensByCustomer(customerId);
} else if (req.user?.role === "admin") {
// Admins can see all tokens
const allCustomers = await storage.getCustomers();
const tokenArrays = await Promise.all(
allCustomers.map(customer => storage.getApiTokensByCustomer(customer.id))
);
tokens = tokenArrays.flat();
} else {
// Others can only see tokens for customers they have access to
const userCustomers = await storage.getUserCustomers(req.user?.id || "");
if (userCustomers.length > 0) {
const tokenArrays = await Promise.all(
userCustomers.map(customer => storage.getApiTokensByCustomer(customer.id))
);
tokens = tokenArrays.flat();
}
}
// Return full tokens (client will handle masking for display)
res.json(tokens);
} 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 {
// Log the full request body first
console.log("Full token creation request body:", JSON.stringify(req.body, null, 2));
// Generate a random token
const tokenValue = randomBytes(32).toString('hex');
// Get data directly from the form
let tokenData: any = {
// Required fields
name: req.body.name || "Unnamed Token",
token: tokenValue,
role: req.body.role || "readonly",
createdBy: req.user?.id || "1",
// Optional fields
isActive: req.body.isActive !== undefined ? req.body.isActive : true,
// Customer access - new field in customer-based model
customerIds: req.body.customerIds || [],
permissions: [] // Will be populated in storage.ts
};
// Handle expiration date based on the expiresIn parameter
const expiresIn = req.body.expiresIn || 'never';
console.log("Processing expiresIn:", expiresIn);
console.log("Full request body for debugging:", req.body);
if (expiresIn !== 'never') {
const now = new Date();
if (expiresIn === 'custom') {
try {
if (req.body.customDate) {
console.log("Custom date received:", req.body.customDate);
// Parse date (format is YYYY-MM-DD from client)
const [year, month, day] = req.body.customDate.split('-').map(Number);
if (isNaN(year) || isNaN(month) || isNaN(day)) {
console.log("Invalid date components:", { year, month, day });
throw new Error("Invalid date format. Expected YYYY-MM-DD");
}
// Create date (months are 0-indexed in JS Date)
const customDate = new Date(year, month - 1, day);
console.log("Parsed customDate:", customDate);
// Add time if specified
if (req.body.customTime) {
console.log("Custom time received:", req.body.customTime);
const [hours, minutes] = req.body.customTime.split(':').map(Number);
if (!isNaN(hours) && !isNaN(minutes)) {
customDate.setHours(hours, minutes, 0, 0);
console.log(`Setting time to ${hours}:${minutes}`);
} else {
customDate.setHours(23, 59, 59, 999);
console.log("Invalid time format, setting to end of day");
}
} else {
customDate.setHours(23, 59, 59, 999);
console.log("No time specified, setting to end of day");
}
// Validate the date is in the future
if (customDate < now) {
throw new Error("Expiration date must be in the future");
}
tokenData.expiresAt = customDate;
console.log("Final custom expiration date:", customDate.toISOString());
} else {
console.log("Missing customDate field in request");
throw new Error("Custom date is required for custom expiration");
}
} catch (e) {
console.error("Error parsing custom date:", e);
return res.status(400).json({
message: "Invalid custom date or time",
details: e instanceof Error ? e.message : String(e),
requestBody: req.body
});
}
} else {
// For preset expiration options
let expirationDate: Date | null = null;
switch (expiresIn) {
case '1hour':
expirationDate = new Date(now.getTime() + 60 * 60 * 1000);
break;
case '1day':
expirationDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
break;
case '7days':
expirationDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
break;
case '30days':
expirationDate = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
break;
case '90days':
expirationDate = new Date(now.getTime() + 90 * 24 * 60 * 60 * 1000);
break;
case '1year':
expirationDate = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
break;
default:
console.log(`Unrecognized expiresIn value: ${expiresIn}, defaulting to no expiration`);
}
if (expirationDate) {
tokenData.expiresAt = expirationDate;
console.log(`Setting expiration to ${expiresIn}:`, expirationDate.toISOString());
}
}
} else {
console.log("No expiration set (never expires)");
}
console.log("Final token data for storage:", JSON.stringify(tokenData, null, 2));
// Pass data directly to storage
const token = await storage.createApiToken(tokenData);
console.log("Token created successfully:", JSON.stringify(token, null, 2));
// Return the full token only on creation
res.status(201).json(token);
} catch (error) {
console.error("Error creating API token:", error);
// Return detailed error to help debugging
res.status(400).json({
message: "Error creating token",
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
}
});
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: admins can modify any token, managers can only modify tokens for customers they have access to
if (req.user?.role !== "admin") {
// Get the customers the user has access to
const userCustomers = await storage.getUserCustomers(req.user?.id || "");
const userCustomerIds = userCustomers.map(c => c.id);
// Get the customers this token has access to
const tokenCustomerAccess = await storage.getApiTokenCustomerAccess(token.id);
const tokenCustomerIds = tokenCustomerAccess.map(a => a.customerId);
// Check if user has access to any of the same customers as the token
const hasAccess = tokenCustomerIds.some(id => userCustomerIds.includes(id));
if (!hasAccess) {
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" });
}
// Return the full token in response (frontend will handle masking for display)
res.json(updatedToken);
} 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" });
}
}
});
// PATCH endpoint for API tokens (for status changes like revocation)
app.patch("/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: admins can modify any token, managers can only modify tokens for customers they have access to
if (req.user?.role !== "admin") {
// Get the customers the user has access to
const userCustomers = await storage.getUserCustomers(req.user?.id || "");
const userCustomerIds = userCustomers.map(c => c.id);
// Get the customers this token has access to
const tokenCustomerAccess = await storage.getApiTokenCustomerAccess(token.id);
const tokenCustomerIds = tokenCustomerAccess.map(a => a.customerId);
// Check if user has access to any of the same customers as the token
const hasAccess = tokenCustomerIds.some(id => userCustomerIds.includes(id));
if (!hasAccess) {
return res.status(403).json({ message: "Insufficient permissions" });
}
}
// For PATCH, we'll allow a simpler validation specifically for isActive status changes
const schema = z.object({
isActive: z.boolean().optional(),
name: z.string().min(1).optional(),
role: z.string().optional(),
});
const validatedData = schema.parse(req.body);
console.log(`PATCH token ${id}:`, validatedData);
const updatedToken = await storage.updateApiToken(id.toString(), validatedData);
if (!updatedToken) {
return res.status(404).json({ message: "API token not found" });
}
// Return the full token in response (frontend will handle masking for display)
res.json(updatedToken);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ message: "Validation error", errors: error.errors });
} else {
console.error("Error updating API token status:", error);
res.status(500).json({ message: "Internal server error", error: error.message });
}
}
});
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: admins can delete any token, managers can only delete tokens for customers they have access to
if (req.user?.role !== "admin") {
// Get the customers the user has access to
const userCustomers = await storage.getUserCustomers(req.user?.id || "");
const userCustomerIds = userCustomers.map(c => c.id);
// Get the customers this token has access to
const tokenCustomerAccess = await storage.getApiTokenCustomerAccess(token.id);
const tokenCustomerIds = tokenCustomerAccess.map(a => a.customerId);
// Check if user has access to any of the same customers as the token
const hasAccess = tokenCustomerIds.some(id => userCustomerIds.includes(id));
if (!hasAccess) {
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 customerId = req.query.customerId as string;
if (customerId) {
// For specific customer, check if user has access to that customer
if (userRole !== "admin") {
const userCustomers = await storage.getUserCustomers(req.user?.id || "");
const userCustomerIds = userCustomers.map(c => c.id);
if (!userCustomerIds.includes(customerId)) {
return res.status(403).json({ message: "Insufficient permissions" });
}
}
webhooks = await storage.getWebhooksByCustomer(customerId);
} else if (userRole === "admin") {
// Admins can see all webhooks
const allCustomers = await storage.getCustomers();
const webhookArrays = await Promise.all(
allCustomers.map(customer => storage.getWebhooksByCustomer(customer.id))
);
webhooks = webhookArrays.flat();
} else {
// Others can only see webhooks for customers they have access to
const userCustomers = await storage.getUserCustomers(req.user?.id || "");
if (userCustomers.length > 0) {
const webhookArrays = await Promise.all(
userCustomers.map(customer => storage.getWebhooksByCustomer(customer.id))
);
webhooks = webhookArrays.flat();
}
}
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 for non-admin users
if (req.user?.role !== "admin") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the webhook's customer
if (!userCustomerIds.includes(webhook.customerId)) {
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") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the customer
if (!userCustomerIds.includes(validatedData.customerId)) {
return res.status(403).json({ message: "Not authorized to create webhooks for this customer" });
}
}
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 for non-admin users
if (req.user?.role !== "admin") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the webhook's customer
if (!userCustomerIds.includes(webhook.customerId)) {
return res.status(403).json({ message: "Not authorized to modify this webhook" });
}
}
const validatedData = insertWebhookSchema.partial().parse(req.body);
// Prevent changing customer for security reasons
if (validatedData.customerId && validatedData.customerId !== webhook.customerId) {
return res.status(400).json({ message: "Cannot change webhook customer" });
}
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 for non-admin users
if (req.user?.role !== "admin") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the webhook's customer
if (!userCustomerIds.includes(webhook.customerId)) {
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 for non-admin users
if (req.user?.role !== "admin") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the webhook's customer
if (!userCustomerIds.includes(webhook.customerId)) {
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 customer's webhooks
if (req.user?.role !== "admin") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the webhook's customer
if (!userCustomerIds.includes(webhook.customerId)) {
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 for non-admin users
if (req.user?.role !== "admin") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the webhook's customer
if (!userCustomerIds.includes(webhook.customerId)) {
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 for non-admin users
if (req.user?.role !== "admin") {
// Get user's customers
const userCustomers = await storage.getUserCustomers(req.user?.id as string);
const userCustomerIds = userCustomers.map(c => c.id);
// Check if the user has access to the webhook's customer
if (!userCustomerIds.includes(webhook.customerId)) {
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;
}