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 { 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 { // 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; }