const fs = require('fs').promises; const state = require('../../../infrastructure/state'); const { getAllEntries, getHashForDomain, doAutoVotes, getConsensusState, invalidateEntriesCache } = require('../../../core/core'); const { addDomain } = require('../../../core/domains'); const { validateDomainAddition, validateDomainRemoval } = require('../../../infrastructure/validation'); const { atomicDomainCleanup } = require('../../../core/domain_cleanup'); const { createInterfaceForDomain } = require('../../../networking/virtual_interfaces'); const { logDebug, logError, logInfo } = require('../../../infrastructure/logger'); const { trackRequest } = require('../../../maintenance/metrics'); const { createErrorResponse } = require('../../../infrastructure/error_handler'); const { broadcast } = require('../websocket'); const { getPersistentPublicKey } = require('../../../infrastructure/utils'); const domainsFile = process.env.DOMAINS_FILE || './cache/domains.json'; async function handleDomainsRoutes(req, res) { const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname; const method = req.method; const url = new URL(req.url, `https://${req.headers.host}`); if (method === 'GET' && urlPath === '/api/resolved-domains') { try { const allEntries = await getAllEntries(); const domainClaimants = new Map(); for (const entry of allEntries) { if (entry.key.startsWith('claim:')) { const parts = entry.key.split(':'); if (parts.length === 3) { const domain = parts[1]; const claimant = parts[2]; if (!domainClaimants.has(domain)) domainClaimants.set(domain, new Set()); domainClaimants.get(domain).add(claimant); } } } const localWriter = getPersistentPublicKey(); const domains = new Set(domainClaimants.keys()); const resolved = []; for (const domain of domains) { const hash = await getHashForDomain(domain) || 'none'; const isLocal = localWriter ? domainClaimants.get(domain)?.has(localWriter) || false : false; let isOwner = false; let consensusState = null; try { consensusState = await getConsensusState(domain); isOwner = localWriter ? consensusState.resolvedClaimant === localWriter : false; } catch (err) { logDebug('Admin', `Error checking ownership/consensus for ${domain}: ${err.message}`); } resolved.push({ domain, hash, isLocal, isOwner, consensusState }); } let internalDomains = ['p2ns.admin']; try { const { getInternalDomains } = require('../../../plugins/plugin-handler'); internalDomains = await getInternalDomains(); } catch (err) { // Fallback if plugin system not available } // Only add internal domains that aren't already in the resolved list for (const d of internalDomains) { if (!resolved.some(r => r.domain === d)) { resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true, consensusStatus: 'internal' }); } } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(resolved.sort((a, b) => a.domain.localeCompare(b.domain)))); } catch (err) { logError('Admin', `Failed to fetch resolved domains: ${err.message}`); res.writeHead(500); res.end(JSON.stringify({ error: 'Failed to fetch domains' })); } return true; } if (method === 'POST' && urlPath === '/api/add-domain') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const data = JSON.parse(body); const validation = validateDomainAddition(data); if (!validation.valid) { res.writeHead(400); res.end(validation.error || 'Invalid input'); return; } // Extract SSL flag - handle both boolean true and string "true" const ssl = data.ssl === true || data.ssl === 'true' || data.ssl === 1; await addDomain(validation.domain, validation.hash, ssl); await doAutoVotes(); let domains = []; if (await fs.access(domainsFile).then(() => true).catch(() => false)) { const parsed = JSON.parse(await fs.readFile(domainsFile, 'utf8')); if (!Array.isArray(parsed)) { logError('Admin', `Domains file does not contain an array, resetting to empty array`); domains = []; } else { domains = parsed; } } const existingIndex = domains.findIndex(d => d.domain === validation.domain); if (existingIndex !== -1) { domains[existingIndex].hash = validation.hash; domains[existingIndex].ssl = ssl; // Update SSL flag } else { domains.push({ domain: validation.domain, hash: validation.hash, ssl: ssl }); } await fs.writeFile(domainsFile, JSON.stringify(domains, null, 2)); if (!state.domainToIPMap.has(validation.domain)) { await createInterfaceForDomain(validation.domain); logDebug('Admin', `Assigned IP to ${validation.domain}: ${state.domainToIPMap.get(validation.domain)}`); } broadcast({ type: 'update-database' }); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } catch (err) { trackRequest('/api/add-domain', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } }); return true; } if (method === 'POST' && urlPath === '/api/remove-domain') { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', async () => { try { const data = JSON.parse(body); const validation = validateDomainRemoval(data); if (!validation.valid) { res.writeHead(400); res.end(validation.error || 'Invalid input'); return; } const domain = validation.domain; // Authorization check: verify peer has claim and is resolved claimant const localWriter = getPersistentPublicKey(); if (!localWriter) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Peer not initialized'); return; } // Check if peer has a claim for this domain const allEntries = await getAllEntries(); const claimKey = `claim:${domain}:${localWriter}`; const hasClaim = allEntries.some(entry => entry.key === claimKey); if (!hasClaim) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Only domains you have claims and resolutions for can be deleted'); return; } // Check if peer is the resolved claimant const consensusState = await getConsensusState(domain); if (consensusState.resolvedClaimant !== localWriter) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Only domains you have claims and resolutions for can be deleted'); return; } await atomicDomainCleanup(domain); if (state.sendRemovalRequest) { state.sendRemovalRequest(domain); } trackRequest('/api/remove-domain', true); broadcast({ type: 'update-database' }); broadcast({ type: 'update-holesail-clients' }); broadcast({ type: 'update-local-dns' }); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK'); } catch (err) { trackRequest('/api/remove-domain', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); } }); return true; } // POST /api/reset-claims-and-entries - Reset all claims and entries, request peers to provide domains if (method === 'POST' && urlPath === '/api/reset-claims-and-entries') { try { trackRequest('/api/reset-claims-and-entries', true); logInfo('Admin', 'Resetting all claims and entries, requesting peers to provide domains'); const pass = state.dnsPass; if (!pass) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'DNS pass not initialized' })); return true; } // Get all entries const allEntries = await getAllEntries(pass, false); // Don't use cache // Filter for claims and votes const entriesToRemove = allEntries.filter(entry => entry.key.startsWith('claim:') || entry.key.startsWith('vote:') ); logInfo('Admin', `Removing ${entriesToRemove.length} claim and vote entries`); // Remove all claim and vote entries let removedCount = 0; let errorCount = 0; for (const entry of entriesToRemove) { try { await pass.remove(entry.key); removedCount++; } catch (err) { logError('Admin', `Error removing entry ${entry.key}: ${err.message}`); errorCount++; } } // Invalidate cache invalidateEntriesCache(); // Send message to all peers requesting them to provide their domains let peersNotified = 0; if (state.peerChannels && state.peerChannels.size > 0) { for (const channels of state.peerChannels.values()) { try { if (channels.requestMessage && channels.requestChannel && !channels.requestChannel.destroyed) { channels.requestMessage.send('request_domains'); peersNotified++; } } catch (err) { logError('Admin', `Error sending request_domains message to peer: ${err.message}`); } } logInfo('Admin', `Sent request_domains message to ${peersNotified} peer(s)`); } // Broadcast updates broadcast({ type: 'update-database' }); broadcast({ type: 'update-stats' }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Claims and entries reset successfully', removed: removedCount, errors: errorCount, peersNotified: peersNotified })); return true; } catch (err) { logError('Admin', `Failed to reset claims and entries: ${err.message}`); trackRequest('/api/reset-claims-and-entries', false); const errorResponse = createErrorResponse(err, 500); res.writeHead(errorResponse.statusCode, errorResponse.headers); res.end(errorResponse.body); return true; } } return false; } module.exports = { handleDomainsRoutes };