- Centralized common formatting, DOM, and status utilities in `includes/plugins/sdk.js` - Created `sdk.utils.format`, `sdk.utils.dom`, and `sdk.utils.status` namespaces - Refactored `domain.consensus` and `peer.visualize` plugins to use the global SDK - Updated `plugin-handler` to serve SDK utilities globally via `/sdk-utils.js` - Enhanced SDK validation by delegating to the core validation infrastructure - Fixed bugs in metric rendering and missing frontend utility functions
593 lines
22 KiB
JavaScript
593 lines
22 KiB
JavaScript
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const state = require('../../../infrastructure/state');
|
|
const { logError, logWarn, logDebug } = require('../../../infrastructure/logger');
|
|
const { getAvailableIPsForSubnet } = require('../../../networking/virtual_interfaces');
|
|
const { settingsMetadata, restartRequiredSettings, liveReloadableSettings, envWhitelist, applyLiveSettings } = require('../settings');
|
|
const { broadcast } = require('../websocket');
|
|
|
|
async function handleSettingsRoutes(req, res) {
|
|
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
|
const method = req.method;
|
|
|
|
if (method === 'GET' && urlPath === '/api/settings') {
|
|
try {
|
|
const settings = {};
|
|
const metadata = {};
|
|
|
|
// Build settings and metadata - always start from settingsMetadata as source of truth
|
|
Object.keys(settingsMetadata).forEach(key => {
|
|
let value = process.env[key] || '';
|
|
// Use default value from metadata if setting is not set
|
|
if (!value && settingsMetadata[key].default) {
|
|
value = settingsMetadata[key].default;
|
|
}
|
|
settings[key] = value;
|
|
|
|
// Always create metadata from the full settingsMetadata object, then add currentValue
|
|
metadata[key] = {
|
|
...settingsMetadata[key], // This includes category, type, label, description, etc.
|
|
currentValue: value || settingsMetadata[key].default || ''
|
|
};
|
|
});
|
|
|
|
// Then, ensure all envWhitelist settings that have metadata are included
|
|
envWhitelist.forEach(key => {
|
|
// Only include settings that have metadata (skip file/directory paths that shouldn't be in UI)
|
|
if (settingsMetadata[key]) {
|
|
if (!settings.hasOwnProperty(key)) {
|
|
settings[key] = process.env[key] || '';
|
|
}
|
|
// If setting has metadata, ensure it's in metadata with full structure
|
|
if (!metadata[key]) {
|
|
metadata[key] = {
|
|
...settingsMetadata[key],
|
|
currentValue: settings[key] || settingsMetadata[key].default || ''
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
// Final pass: Ensure ALL settingsMetadata entries are in metadata with complete structure
|
|
Object.keys(settingsMetadata).forEach(key => {
|
|
const currentValue = metadata[key]?.currentValue || settings[key] || settingsMetadata[key].default || '';
|
|
metadata[key] = {
|
|
...settingsMetadata[key], // Complete source structure (includes category, type, label, etc.)
|
|
currentValue: currentValue // Preserve the current value
|
|
};
|
|
});
|
|
|
|
// Ensure settings exist for all metadata entries
|
|
Object.keys(settingsMetadata).forEach(key => {
|
|
if (!settings.hasOwnProperty(key)) {
|
|
settings[key] = process.env[key] || settingsMetadata[key].default || '';
|
|
}
|
|
});
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ settings, metadata }));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to fetch settings: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: 'Failed to fetch settings' }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (method === 'GET' && urlPath === '/api/subnets') {
|
|
try {
|
|
let subnets = [];
|
|
if (process.env.SUBNETS) {
|
|
try {
|
|
subnets = JSON.parse(process.env.SUBNETS);
|
|
if (!Array.isArray(subnets)) {
|
|
subnets = [];
|
|
}
|
|
} catch (err) {
|
|
logWarn('Admin', `Failed to parse SUBNETS: ${err.message}`);
|
|
subnets = [];
|
|
}
|
|
}
|
|
|
|
if (subnets.length === 0) {
|
|
const subnetBase = process.env.SUBNET_BASE || '192.168.3';
|
|
const baseParts = subnetBase.split('.');
|
|
if (baseParts.length === 3) {
|
|
subnets = [{
|
|
base: `${subnetBase}.0`,
|
|
cidr: 24,
|
|
startIndex: parseInt(process.env.INITIAL_IP_INDEX || '2', 10),
|
|
name: 'Default Subnet'
|
|
}];
|
|
}
|
|
}
|
|
|
|
const subnetInfo = subnets.map((subnet, index) => {
|
|
const available = getAvailableIPsForSubnet(subnet);
|
|
const used = Array.from(state.domainToIPMap.values()).filter(ip => {
|
|
const ipParts = ip.split('.');
|
|
const subnetParts = subnet.base.split('.');
|
|
return ipParts[0] === subnetParts[0] &&
|
|
ipParts[1] === subnetParts[1] &&
|
|
ipParts[2] === subnetParts[2];
|
|
}).length;
|
|
return {
|
|
...subnet,
|
|
index,
|
|
available,
|
|
used,
|
|
remaining: Math.max(0, available - used)
|
|
};
|
|
});
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ subnets: subnetInfo }));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to fetch subnets: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: 'Failed to fetch subnets' }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/subnets') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { subnets } = JSON.parse(body);
|
|
|
|
if (!Array.isArray(subnets)) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'subnets must be an array' }));
|
|
return;
|
|
}
|
|
|
|
const errors = [];
|
|
subnets.forEach((subnet, index) => {
|
|
if (!subnet || typeof subnet !== 'object') {
|
|
errors.push(`subnets[${index}]: must be an object`);
|
|
return;
|
|
}
|
|
|
|
if (!subnet.base || typeof subnet.base !== 'string') {
|
|
errors.push(`subnets[${index}]: base is required and must be a string`);
|
|
} else if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(subnet.base)) {
|
|
errors.push(`subnets[${index}]: base must be a valid IPv4 address`);
|
|
}
|
|
|
|
const cidr = parseInt(subnet.cidr, 10);
|
|
if (isNaN(cidr) || cidr < 1 || cidr > 32) {
|
|
errors.push(`subnets[${index}]: cidr must be between 1 and 32`);
|
|
}
|
|
|
|
const startIndex = parseInt(subnet.startIndex || process.env.INITIAL_IP_INDEX || '2', 10);
|
|
const maxIPs = Math.pow(2, 32 - cidr) - 2;
|
|
if (isNaN(startIndex) || startIndex < 1 || startIndex > Math.min(254, maxIPs)) {
|
|
errors.push(`subnets[${index}]: startIndex must be between 1 and ${Math.min(254, maxIPs)}`);
|
|
}
|
|
|
|
if (!subnet.name || typeof subnet.name !== 'string') {
|
|
subnet.name = `Subnet ${index + 1}`;
|
|
}
|
|
});
|
|
|
|
if (errors.length > 0) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Validation failed', errors }));
|
|
return;
|
|
}
|
|
|
|
process.env.SUBNETS = JSON.stringify(subnets);
|
|
|
|
const envContent = envWhitelist.map(key => {
|
|
if (key === 'SUBNETS') {
|
|
return `${key}=${JSON.stringify(subnets)}`;
|
|
}
|
|
return `${key}=${process.env[key] || ''}`;
|
|
}).join('\n');
|
|
await fs.writeFile('.env', envContent);
|
|
|
|
state.subnets = subnets;
|
|
state.currentSubnetIndex = 0;
|
|
state.subnetIPCounters.clear();
|
|
subnets.forEach((subnet, index) => {
|
|
state.subnetIPCounters.set(index, subnet.startIndex || parseInt(process.env.INITIAL_IP_INDEX || '2', 10));
|
|
});
|
|
|
|
broadcast({ type: 'update-settings' });
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
message: 'Subnets updated. Restart required to fully apply changes.',
|
|
restartRequired: true
|
|
}));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to update subnets: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/update-settings') {
|
|
let body = '';
|
|
req.on('data', chunk => { body += chunk; });
|
|
req.on('end', async () => {
|
|
try {
|
|
const { settings } = JSON.parse(body);
|
|
const errors = [];
|
|
|
|
for (const [key, value] of Object.entries(settings)) {
|
|
if (!envWhitelist.includes(key)) {
|
|
errors.push(`Setting ${key} is not whitelisted`);
|
|
continue;
|
|
}
|
|
|
|
const meta = settingsMetadata[key];
|
|
if (meta) {
|
|
if (meta.type === 'number') {
|
|
const numValue = parseInt(value, 10);
|
|
if (isNaN(numValue)) {
|
|
errors.push(`${meta.label}: must be a number`);
|
|
continue;
|
|
}
|
|
if (meta.min !== undefined && numValue < meta.min) {
|
|
errors.push(`${meta.label}: must be at least ${meta.min}`);
|
|
continue;
|
|
}
|
|
if (meta.max !== undefined && numValue > meta.max) {
|
|
errors.push(`${meta.label}: must be at most ${meta.max}`);
|
|
continue;
|
|
}
|
|
process.env[key] = numValue.toString();
|
|
} else if (meta.type === 'checkbox') {
|
|
process.env[key] = (value === true || value === 'true' || value === '1') ? 'true' : 'false';
|
|
} else if (key === 'SUBNETS') {
|
|
try {
|
|
const subnets = typeof value === 'string' ? JSON.parse(value) : value;
|
|
if (!Array.isArray(subnets)) {
|
|
errors.push('SUBNETS must be an array');
|
|
continue;
|
|
}
|
|
process.env[key] = JSON.stringify(subnets);
|
|
} catch (err) {
|
|
errors.push(`SUBNETS: invalid JSON - ${err.message}`);
|
|
continue;
|
|
}
|
|
} else if (key === 'PUBLIC_DNS_SERVER') {
|
|
// Validate comma-separated IP addresses
|
|
const dnsServers = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
if (dnsServers.length === 0) {
|
|
errors.push(`${meta.label}: at least one DNS server is required`);
|
|
continue;
|
|
}
|
|
const invalidServers = [];
|
|
dnsServers.forEach((server, index) => {
|
|
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(server)) {
|
|
invalidServers.push(`server ${index + 1} (${server})`);
|
|
}
|
|
});
|
|
if (invalidServers.length > 0) {
|
|
errors.push(`${meta.label}: invalid IP addresses: ${invalidServers.join(', ')}`);
|
|
continue;
|
|
}
|
|
process.env[key] = value;
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
} else {
|
|
if (key === 'SUBNETS') {
|
|
try {
|
|
const subnets = typeof value === 'string' ? JSON.parse(value) : value;
|
|
if (!Array.isArray(subnets)) {
|
|
errors.push('SUBNETS must be an array');
|
|
continue;
|
|
}
|
|
process.env[key] = JSON.stringify(subnets);
|
|
} catch (err) {
|
|
errors.push(`SUBNETS: invalid JSON - ${err.message}`);
|
|
continue;
|
|
}
|
|
} else if (key === 'PUBLIC_DNS_SERVER') {
|
|
// Validate comma-separated IP addresses
|
|
const dnsServers = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
if (dnsServers.length === 0) {
|
|
errors.push(`PUBLIC_DNS_SERVER: at least one DNS server is required`);
|
|
continue;
|
|
}
|
|
const invalidServers = [];
|
|
dnsServers.forEach((server, index) => {
|
|
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(server)) {
|
|
invalidServers.push(`server ${index + 1} (${server})`);
|
|
}
|
|
});
|
|
if (invalidServers.length > 0) {
|
|
errors.push(`PUBLIC_DNS_SERVER: invalid IP addresses: ${invalidServers.join(', ')}`);
|
|
continue;
|
|
}
|
|
process.env[key] = value;
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Validation failed', errors }));
|
|
return;
|
|
}
|
|
|
|
const envContent = envWhitelist.map(key => {
|
|
if (key === 'SUBNETS') {
|
|
return `${key}=${process.env[key] || '[]'}`;
|
|
}
|
|
return `${key}=${process.env[key] || ''}`;
|
|
}).join('\n');
|
|
await fs.writeFile('.env', envContent);
|
|
|
|
const restartRequired = Object.keys(settings).some(key => restartRequiredSettings.includes(key));
|
|
|
|
const liveSettings = {};
|
|
for (const [key, value] of Object.entries(settings)) {
|
|
if (liveReloadableSettings.includes(key)) {
|
|
liveSettings[key] = value;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(liveSettings).length > 0) {
|
|
await applyLiveSettings(liveSettings);
|
|
}
|
|
|
|
broadcast({ type: 'update-settings' });
|
|
|
|
if (restartRequired) {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
message: 'Settings saved. Some settings require restart to take effect.',
|
|
restartRequired: true,
|
|
restartRequiredSettings: Object.keys(settings).filter(k => restartRequiredSettings.includes(k))
|
|
}));
|
|
} else {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
message: 'Settings saved and applied successfully.',
|
|
restartRequired: false
|
|
}));
|
|
}
|
|
} catch (err) {
|
|
logError('Admin', `Failed to update settings: ${err.message}`);
|
|
res.writeHead(500);
|
|
res.end(err.message);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (method === 'GET' && urlPath === '/api/network-interfaces') {
|
|
try {
|
|
const networkInterfaces = os.networkInterfaces();
|
|
const interfaces = [];
|
|
const interfaceSet = new Set();
|
|
|
|
// Get OS-specific default interface name
|
|
const osDefault = os.platform() === 'darwin' ? 'lo0'
|
|
: os.platform() === 'linux' ? 'lo'
|
|
: os.platform() === 'win32' ? 'Loopback Pseudo-Interface 1'
|
|
: '';
|
|
|
|
// Collect all interface names
|
|
for (const [name, addresses] of Object.entries(networkInterfaces)) {
|
|
if (!addresses || addresses.length === 0) continue;
|
|
if (!interfaceSet.has(name)) {
|
|
interfaceSet.add(name);
|
|
interfaces.push({
|
|
value: name,
|
|
label: name
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort interfaces, prioritizing loopback interfaces (lo, lo0) and the OS default
|
|
interfaces.sort((a, b) => {
|
|
const aIsDefault = a.value === osDefault;
|
|
const bIsDefault = b.value === osDefault;
|
|
if (aIsDefault && !bIsDefault) return -1;
|
|
if (!aIsDefault && bIsDefault) return 1;
|
|
|
|
const aIsLoopback = a.value.startsWith('lo');
|
|
const bIsLoopback = b.value.startsWith('lo');
|
|
if (aIsLoopback && !bIsLoopback) return -1;
|
|
if (!aIsLoopback && bIsLoopback) return 1;
|
|
|
|
return a.value.localeCompare(b.value);
|
|
});
|
|
|
|
// Ensure OS default is in the list if it exists
|
|
if (osDefault && !interfaceSet.has(osDefault)) {
|
|
interfaces.unshift({
|
|
value: osDefault,
|
|
label: `${osDefault} (default)`
|
|
});
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ interfaces }));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to fetch network interfaces: ${err.message}`);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (method === 'GET' && urlPath === '/api/identity') {
|
|
try {
|
|
const { getPersistentPublicKey } = require('../../../infrastructure/utils');
|
|
const publicKey = getPersistentPublicKey();
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
publicKey: publicKey || null,
|
|
hasIdentity: !!publicKey
|
|
}));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to fetch identity: ${err.message}`);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (method === 'POST' && urlPath === '/api/reset-identity') {
|
|
try {
|
|
const keypairPath = path.resolve('./cache/keypair.json');
|
|
|
|
// Check if keypair file exists
|
|
try {
|
|
await fs.access(keypairPath);
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Keypair file not found' }));
|
|
return true;
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
// Delete the keypair file
|
|
await fs.unlink(keypairPath);
|
|
logWarn('Admin', 'Identity keypair deleted by user');
|
|
|
|
// Clear the keypair from state
|
|
state.keypair = null;
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
message: 'Identity reset successfully. A new keypair will be generated on next restart. Please restart the application for the changes to take effect.'
|
|
}));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to reset identity: ${err.message}`);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// POST /api/recheck-peers - Force peer recheck/rediscovery
|
|
if (method === 'POST' && urlPath === '/api/recheck-peers') {
|
|
try {
|
|
const crypto = require('crypto');
|
|
const { logInfo } = require('../../../infrastructure/logger');
|
|
|
|
if (!state.swarm) {
|
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Swarm not initialized' }));
|
|
return true;
|
|
}
|
|
|
|
// Get the topic
|
|
const TOPIC_SEED = process.env.TOPIC_SEED || 'p2ns-dns';
|
|
const topic = crypto.createHash('sha256').update(TOPIC_SEED).digest();
|
|
|
|
// Flush the DHT to force a recheck for peers
|
|
logInfo('Admin', 'Forcing peer recheck via swarm.flush()');
|
|
await state.swarm.flush();
|
|
|
|
// Rejoin the topic to trigger new peer discovery
|
|
// Note: join() is idempotent, so calling it again is safe
|
|
state.swarm.join(topic, { server: true, client: true });
|
|
logInfo('Admin', 'Rejoined Hyperswarm topic to trigger peer discovery');
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
message: 'Peer recheck initiated. New peers will be discovered shortly.'
|
|
}));
|
|
} catch (err) {
|
|
logError('Admin', `Failed to recheck peers: ${err.message}`);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: `Failed to recheck peers: ${err.message}` }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// POST /api/admin/clean-dns-storage - Clean DNS Pass storage and reset initialization
|
|
if (method === 'POST' && urlPath === '/api/admin/clean-dns-storage') {
|
|
try {
|
|
const { logInfo, logWarn, logError } = require('../../../infrastructure/logger');
|
|
|
|
// Safety check: warn if there are active connections
|
|
if (state.connectedPeers && state.connectedPeers.size > 0) {
|
|
logWarn('Admin', `Cleaning storage while ${state.connectedPeers.size} peers are connected - this may disrupt the network`);
|
|
}
|
|
|
|
// Safety check: warn if this is a master node with initialized DNS pass
|
|
if (state.isMaster && state.dnsPass) {
|
|
logWarn('Admin', 'Cleaning storage on a master node that has initialized DNS pass - other nodes may be affected');
|
|
}
|
|
|
|
logWarn('Admin', 'Cleaning DNS Pass storage as requested by user');
|
|
|
|
// Get storage directory
|
|
const storageDir = process.env.STORAGE_DIR || './my-storage';
|
|
|
|
// Clean the storage directory (same logic as --clean flag)
|
|
try {
|
|
await fs.rm(storageDir, { recursive: true, force: true });
|
|
logInfo('Admin', `Cleaned DNS Pass storage directory: ${storageDir}`);
|
|
} catch (err) {
|
|
// Ignore if directory doesn't exist
|
|
if (err.code !== 'ENOENT') {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Reset state variables that depend on the storage
|
|
state.dnsPass = null;
|
|
state.consecutiveInviteFailures = 0; // Reset failure counter
|
|
|
|
// Note: We don't reset the keypair or other persistent state, only the DNS Pass data
|
|
|
|
logInfo('Admin', 'DNS Pass storage cleaned and state reset successfully');
|
|
|
|
// Send success response before shutting down
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
message: 'DNS Pass storage cleaned. Shutting down for reinitialization...',
|
|
storageDir: storageDir,
|
|
shutdownInitiated: true,
|
|
restartInstructions: 'Process will shut down. Restart manually with: sudo node p2ns.js' + (state.isMaster ? ' --master' : '')
|
|
}));
|
|
|
|
// Trigger graceful shutdown after sending response
|
|
// Add a longer delay to ensure storage cleanup is complete and state is reset
|
|
setTimeout(() => {
|
|
logInfo('Admin', 'Initiating graceful shutdown after DNS storage cleanup...');
|
|
logInfo('Admin', '🔄 SYSTEM IS SHUTTING DOWN - Storage has been cleaned and will reinitialize on restart');
|
|
process.emit('SIGTERM');
|
|
}, 3000); // Give more time for response to be sent and cleanup to complete
|
|
} catch (err) {
|
|
logError('Admin', `Failed to clean DNS storage: ${err.message}`);
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: `Failed to clean DNS storage: ${err.message}` }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
module.exports = { handleSettingsRoutes };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|