diff --git a/dashcaddy-api/routes/dns.js b/dashcaddy-api/routes/dns.js index 498c7d5..b124484 100644 --- a/dashcaddy-api/routes/dns.js +++ b/dashcaddy-api/routes/dns.js @@ -4,105 +4,126 @@ const fsp = require('fs').promises; const validatorLib = require('validator'); const { APP, TIMEOUTS, CADDY, DNS_RECORD_TYPES, REGEX, SESSION_TTL } = require('../constants'); const { exists } = require('../fs-helpers'); +const { success, error: errorResponse } = require('../response-helpers'); -module.exports = function(ctx) { +/** + * DNS routes factory + * @param {Object} deps - Explicit dependencies + * @param {Object} deps.dns - DNS management interface (call, requireToken, getToken, etc.) + * @param {Object} deps.siteConfig - Site configuration + * @param {Function} deps.asyncHandler - Async route handler wrapper + * @param {Object} deps.log - Logger instance + * @param {Function} deps.safeErrorMessage - Safe error message extractor + * @param {Function} deps.fetchT - Fetch wrapper with timeout + * @param {Object} deps.credentialManager - Credential storage manager + * @returns {express.Router} + */ +module.exports = function({ + dns, + siteConfig, + asyncHandler, + log, + safeErrorMessage, + fetchT, + credentialManager +}) { const router = express.Router(); /** Validate that a server IP is in the configured DNS servers list */ function validateDnsServer(server) { const serverIp = server.includes(':') ? server.split(':')[0] : server; if (!validatorLib.isIP(serverIp)) return null; - const configuredIps = Object.values(ctx.siteConfig.dnsServers || {}).map(s => s.ip).filter(Boolean); + const configuredIps = Object.values(siteConfig.dnsServers || {}).map(s => s.ip).filter(Boolean); // Also allow the default dnsServerIp - if (ctx.siteConfig.dnsServerIp) configuredIps.push(ctx.siteConfig.dnsServerIp); + if (siteConfig.dnsServerIp) configuredIps.push(siteConfig.dnsServerIp); if (!configuredIps.includes(serverIp)) return null; return serverIp; } // DELETE /record — Delete a DNS record from Technitium - router.delete('/record', ctx.asyncHandler(async (req, res) => { + router.delete('/record', asyncHandler(async (req, res) => { const { domain, type, token, server, ipAddress } = req.query; - const dnsToken = await ctx.dns.requireToken(token); + const dnsToken = await dns.requireToken(token); if (!domain) { - return ctx.errorResponse(res, 400, 'domain is required'); + return errorResponse(res, 'domain is required', 400); } // Validate domain format if (!REGEX.DOMAIN.test(domain)) { - return ctx.errorResponse(res, 400, '[DC-301] Invalid domain format'); + return errorResponse(res, '[DC-301] Invalid domain format', 400); } // Validate record type if (type && !DNS_RECORD_TYPES.includes(type.toUpperCase())) { - return ctx.errorResponse(res, 400, 'Invalid DNS record type'); + return errorResponse(res, 'Invalid DNS record type', 400); } // Validate ipAddress if provided if (ipAddress && !validatorLib.isIP(ipAddress)) { - return ctx.errorResponse(res, 400, '[DC-210] Invalid IP address'); + return errorResponse(res, '[DC-210] Invalid IP address', 400); } // Validate server against configured DNS servers if (server && !validateDnsServer(server)) { - return ctx.errorResponse(res, 400, 'Server must be a configured DNS server'); + return errorResponse(res, 'Server must be a configured DNS server', 400); } // Default to dns1 LAN IP, allow override - const dnsServer = server || ctx.siteConfig.dnsServerIp; + const dnsServer = server || siteConfig.dnsServerIp; const recordType = type || 'A'; try { const p = { token: dnsToken, domain: domain, type: recordType }; if (ipAddress) p.ipAddress = ipAddress; - const result = await ctx.dns.call(dnsServer, '/api/zones/records/delete', p); + const result = await dns.call(dnsServer, '/api/zones/records/delete', p); if (result.status === 'ok') { - res.json({ success: true, message: `DNS record ${domain} deleted` }); + success(res, { message: `DNS record ${domain} deleted` }); } else { - ctx.errorResponse(res, 500, result.errorMessage || 'DNS deletion failed'); + errorResponse(res, result.errorMessage || 'DNS deletion failed', 500); } } catch (error) { - ctx.errorResponse(res, 500, ctx.safeErrorMessage(error)); + errorResponse(res, safeErrorMessage(error), 500); } }, 'dns-delete-record')); // POST /record — Create a DNS record in Technitium - router.post('/record', ctx.asyncHandler(async (req, res) => { + router.post('/record', asyncHandler(async (req, res) => { const { domain, ip, ttl, token, server } = req.body; - const dnsToken = await ctx.dns.requireToken(token); + const dnsToken = await dns.requireToken(token); if (!domain || !ip) { - return ctx.errorResponse(res, 400, 'domain and ip are required'); + return errorResponse(res, 'domain and ip are required', 400); } // Validate domain format if (!REGEX.DOMAIN.test(domain)) { - return ctx.errorResponse(res, 400, '[DC-301] Invalid domain format'); + return errorResponse(res, '[DC-301] Invalid domain format', 400); } // Validate IP address if (!validatorLib.isIP(ip)) { - return ctx.errorResponse(res, 400, '[DC-210] Invalid IP address'); + return errorResponse(res, '[DC-210] Invalid IP address', 400); } // Validate TTL if provided if (ttl !== undefined) { const parsedTtl = parseInt(ttl, 10); if (isNaN(parsedTtl) || parsedTtl < CADDY.TTL_MIN || parsedTtl > CADDY.TTL_MAX) { - return ctx.errorResponse(res, 400, `TTL must be between ${CADDY.TTL_MIN} and ${CADDY.TTL_MAX}`); + return errorResponse(res, `TTL must be between ${CADDY.TTL_MIN} and ${CADDY.TTL_MAX}`, 400); } } // Validate server against configured DNS servers if (server && !validateDnsServer(server)) { - return ctx.errorResponse(res, 400, 'Server must be a configured DNS server'); + return errorResponse(res, 'Server must be a configured DNS server', 400); } // Default to dns1 LAN IP since Docker container can't access Tailscale network - const dnsServer = server || ctx.siteConfig.dnsServerIp; + const dnsServer = server || siteConfig.dnsServerIp; const recordTtl = ttl || 300; try { @@ -110,48 +131,48 @@ module.exports = function(ctx) { // domain = "test.sami" -> zone = "sami", subdomain = "test" const parts = domain.split('.'); const subdomain = parts[0]; - const zone = parts.slice(1).join('.') || ctx.siteConfig.tld.replace(/^\./, ''); + const zone = parts.slice(1).join('.') || siteConfig.tld.replace(/^\./, ''); - const result = await ctx.dns.call(dnsServer, '/api/zones/records/add', { + const result = await dns.call(dnsServer, '/api/zones/records/add', { token: dnsToken, domain, zone, type: 'A', ipAddress: ip, ttl: recordTtl.toString(), overwrite: 'true' }); if (result.status === 'ok') { - res.json({ success: true, message: `DNS record ${domain} -> ${ip} created` }); + success(res, { message: `DNS record ${domain} -> ${ip} created` }); } else { - ctx.errorResponse(res, 500, result.errorMessage || 'DNS creation failed'); + errorResponse(res, result.errorMessage || 'DNS creation failed', 500); } } catch (error) { - ctx.log.error('dns', 'DNS record creation error', { error: error.message }); - ctx.errorResponse(res, 500, ctx.safeErrorMessage(error), { details: error.cause?.code || 'fetch failed' }); + log.error('dns', 'DNS record creation error', { error: error.message }); + errorResponse(res, safeErrorMessage(error), 500, { details: error.cause?.code || 'fetch failed' }); } }, 'dns-create-record')); // GET /resolve — Resolve a domain to IP address via Technitium - router.get('/resolve', ctx.asyncHandler(async (req, res) => { + router.get('/resolve', asyncHandler(async (req, res) => { const { domain, server, token } = req.query; - const dnsToken = await ctx.dns.requireToken(token); + const dnsToken = await dns.requireToken(token); if (!domain) { - return ctx.errorResponse(res, 400, 'domain is required'); + return errorResponse(res, 'domain is required', 400); } // Validate domain format if (!REGEX.DOMAIN.test(domain)) { - return ctx.errorResponse(res, 400, '[DC-301] Invalid domain format'); + return errorResponse(res, '[DC-301] Invalid domain format', 400); } // Validate server against configured DNS servers if (server && !validateDnsServer(server)) { - return ctx.errorResponse(res, 400, 'Server must be a configured DNS server'); + return errorResponse(res, 'Server must be a configured DNS server', 400); } - const dnsServer = server || ctx.siteConfig.dnsServerIp; + const dnsServer = server || siteConfig.dnsServerIp; try { - const result = await ctx.dns.call(dnsServer, '/api/zones/records/get', { - token: dnsToken, domain, zone: ctx.siteConfig.tld.replace(/^\./, ''), listZone: 'true' + const result = await dns.call(dnsServer, '/api/zones/records/get', { + token: dnsToken, domain, zone: siteConfig.tld.replace(/^\./, ''), listZone: 'true' }); if (result.status === 'ok' && result.response && result.response.records) { @@ -159,47 +180,47 @@ module.exports = function(ctx) { const aRecords = result.response.records.filter(r => r.type === 'A'); if (aRecords.length > 0) { const ipAddresses = aRecords.map(r => r.rData?.ipAddress).filter(Boolean); - res.json({ success: true, answer: ipAddresses }); + success(res, { answer: ipAddresses }); } else { - ctx.errorResponse(res, 404, 'No A records found for domain'); + errorResponse(res, 'No A records found for domain', 404); } } else { - ctx.errorResponse(res, 500, result.errorMessage || 'DNS resolve failed'); + errorResponse(res, result.errorMessage || 'DNS resolve failed', 500); } } catch (error) { - ctx.log.error('dns', 'DNS resolve error', { error: error.message }); - ctx.errorResponse(res, 500, ctx.safeErrorMessage(error)); + log.error('dns', 'DNS resolve error', { error: error.message }); + errorResponse(res, safeErrorMessage(error), 500); } }, 'dns-resolve')); // GET /logs — Fetch DNS query logs from Technitium - router.get('/logs', ctx.asyncHandler(async (req, res) => { + router.get('/logs', asyncHandler(async (req, res) => { const { server, limit } = req.query; if (!server) { - return ctx.errorResponse(res, 400, 'server is required'); + return errorResponse(res, 'server is required', 400); } // Validate server against configured DNS servers const serverIp = validateDnsServer(server); if (!serverIp) { - return ctx.errorResponse(res, 400, 'Server must be a configured DNS server'); + return errorResponse(res, 'Server must be a configured DNS server', 400); } const logLimit = Math.min(parseInt(limit) || 25, 1000); - const dnsPort = ctx.siteConfig.dnsServerPort || '5380'; + const dnsPort = siteConfig.dnsServerPort || '5380'; try { // Auto-authenticate using stored read-only credentials for log access - const authResult = await ctx.dns.getTokenForServer(serverIp, 'readonly'); + const authResult = await dns.getTokenForServer(serverIp, 'readonly'); if (!authResult.success) { - return ctx.errorResponse(res, 401, 'DNS auto-authentication failed. Ensure credentials are configured via the DNS panel.'); + return errorResponse(res, 'DNS auto-authentication failed. Ensure credentials are configured via the DNS panel.', 401); } const effectiveToken = authResult.token; // Try to get available log files first const listUrl = `http://${serverIp}:${dnsPort}/api/logs/list?token=${encodeURIComponent(effectiveToken)}`; - const listResponse = await ctx.fetchT(listUrl, { method: 'GET', headers: { 'Accept': 'application/json' } }); + const listResponse = await fetchT(listUrl, { method: 'GET', headers: { 'Accept': 'application/json' } }); let logFileName = new Date().toISOString().split('T')[0]; // Default to today @@ -213,9 +234,9 @@ module.exports = function(ctx) { // Technitium logs/download endpoint - returns plain text logs const technitiumUrl = `http://${serverIp}:${dnsPort}/api/logs/download?token=${encodeURIComponent(effectiveToken)}&fileName=${logFileName}`; - ctx.log.info('dns', 'Fetching DNS logs', { server, logFileName }); + log.info('dns', 'Fetching DNS logs', { server, logFileName }); - const response = await ctx.fetchT(technitiumUrl, { + const response = await fetchT(technitiumUrl, { method: 'GET', headers: { 'Accept': 'text/plain' }, timeout: 10000 @@ -227,17 +248,16 @@ module.exports = function(ctx) { try { const errorJson = JSON.parse(errorText); if (errorJson.errorMessage?.includes('Could not find file')) { - return res.json({ - success: true, + return success(res, { server: server, count: 0, logs: [], message: 'No logs available for this server' }); } - return ctx.errorResponse(res, response.status, ctx.safeErrorMessage(errorJson.errorMessage || errorText)); + return errorResponse(res, safeErrorMessage(errorJson.errorMessage || errorText), response.status); } catch { - return ctx.errorResponse(res, response.status, 'DNS server returned an error'); + return errorResponse(res, 'DNS server returned an error', response.status); } } @@ -250,8 +270,7 @@ module.exports = function(ctx) { const errorJson = JSON.parse(logText); if (errorJson.status && errorJson.status !== 'ok') { if (errorJson.errorMessage?.includes('Could not find file')) { - return res.json({ - success: true, + return success(res, { server: server, count: 0, logs: [], @@ -260,9 +279,9 @@ module.exports = function(ctx) { } // Invalidate cached token on auth errors so next request re-authenticates if (errorJson.status === 'invalid-token') { - ctx.dns.invalidateTokenForServer(serverIp); + dns.invalidateTokenForServer(serverIp); } - return ctx.errorResponse(res, 400, ctx.safeErrorMessage(errorJson.errorMessage)); + return errorResponse(res, safeErrorMessage(errorJson.errorMessage), 400); } } catch { /* Not JSON, continue parsing as text */ } } @@ -293,9 +312,8 @@ module.exports = function(ctx) { return { raw: line, parsed: false }; }).reverse(); // Reverse to show most recent first - ctx.log.info('dns', 'Returning DNS log entries', { count: parsedLogs.length, logFileName }); - res.json({ - success: true, + log.info('dns', 'Returning DNS log entries', { count: parsedLogs.length, logFileName }); + success(res, { server: server, logFile: logFileName, count: parsedLogs.length, @@ -303,33 +321,32 @@ module.exports = function(ctx) { }); } catch (error) { - ctx.log.error('dns', 'DNS logs proxy error', { error: error.message }); - ctx.errorResponse(res, 500, ctx.safeErrorMessage(error)); + log.error('dns', 'DNS logs proxy error', { error: error.message }); + errorResponse(res, safeErrorMessage(error), 500); } }, 'dns-logs')); // GET /token-status — Check DNS token/credentials status - router.get('/token-status', ctx.asyncHandler(async (req, res) => { - const username = await ctx.credentialManager.retrieve('dns.username'); - const hasCredentials = !!username || await exists(ctx.dns.credentialsFile); - const hasToken = !!ctx.dns.getToken(); + router.get('/token-status', asyncHandler(async (req, res) => { + const username = await credentialManager.retrieve('dns.username'); + const hasCredentials = !!username || await exists(dns.credentialsFile); + const hasToken = !!dns.getToken(); - res.json({ - success: true, + success(res, { hasCredentials, hasToken, - tokenExpiry: ctx.dns.getTokenExpiry(), - isExpired: ctx.dns.getTokenExpiry() ? new Date() > new Date(ctx.dns.getTokenExpiry()) : null + tokenExpiry: dns.getTokenExpiry(), + isExpired: dns.getTokenExpiry() ? new Date() > new Date(dns.getTokenExpiry()) : null }); }, 'dns-token-status')); // POST /credentials — Store DNS credentials (encrypted) // Accepts per-server format: { servers: { dns1: { username, password }, dns2: {...}, dns3: {...} } } // Also accepts legacy format: { username, password, server } - router.post('/credentials', ctx.asyncHandler(async (req, res) => { + router.post('/credentials', asyncHandler(async (req, res) => { const { servers, username, password, server } = req.body; const dangerousChars = [';', '&', '|', '`', '$', '\n', '\r']; - const dnsPort = ctx.siteConfig.dnsServerPort || '5380'; + const dnsPort = siteConfig.dnsServerPort || '5380'; // Per-server format: { servers: { dns1: { readonly: { username, password }, admin: { username, password } }, ... } } if (servers && typeof servers === 'object') { @@ -338,7 +355,7 @@ module.exports = function(ctx) { for (const [dnsId, creds] of Object.entries(servers)) { // Look up server IP from config - const serverInfo = ctx.siteConfig.dnsServers?.[dnsId]; + const serverInfo = siteConfig.dnsServers?.[dnsId]; const serverIp = serverInfo?.ip; if (!serverIp) { results[dnsId] = { success: false, error: `No IP configured for ${dnsId}` }; @@ -363,13 +380,13 @@ module.exports = function(ctx) { // Test credentials by logging in to the target server try { - const testResult = await ctx.dns.refresh(typeCreds.username, typeCreds.password, serverIp); + const testResult = await dns.refresh(typeCreds.username, typeCreds.password, serverIp); if (testResult.success) { - await ctx.credentialManager.store(`dns.${dnsId}.${credType}.username`, typeCreds.username, { type: 'dns', role: credType, server: serverIp }); - await ctx.credentialManager.store(`dns.${dnsId}.${credType}.password`, typeCreds.password, { type: 'dns', role: credType, server: serverIp }); + await credentialManager.store(`dns.${dnsId}.${credType}.username`, typeCreds.username, { type: 'dns', role: credType, server: serverIp }); + await credentialManager.store(`dns.${dnsId}.${credType}.password`, typeCreds.password, { type: 'dns', role: credType, server: serverIp }); savedTypes.push(credType); anySuccess = true; - ctx.log.info('dns', `${credType} credentials saved for ${dnsId}`, { server: serverIp }); + log.info('dns', `${credType} credentials saved for ${dnsId}`, { server: serverIp }); } else { if (!results[dnsId]) { results[dnsId] = { success: false, error: `${credType}: ${testResult.error || 'Login failed'}` }; @@ -400,132 +417,130 @@ module.exports = function(ctx) { // Legacy single-credential format: { username, password, server } if (!username || !password) { - return ctx.errorResponse(res, 400, 'username and password are required'); + return errorResponse(res, 'username and password are required', 400); } if (username.length > 100 || password.length > 512) { - return ctx.errorResponse(res, 400, 'Credentials exceed maximum length'); + return errorResponse(res, 'Credentials exceed maximum length', 400); } if (dangerousChars.some(char => username.includes(char))) { - return ctx.errorResponse(res, 400, 'Username contains invalid characters'); + return errorResponse(res, 'Username contains invalid characters', 400); } if (server && !validateDnsServer(server)) { - return ctx.errorResponse(res, 400, 'Server must be a configured DNS server'); + return errorResponse(res, 'Server must be a configured DNS server', 400); } - const testResult = await ctx.dns.refresh(username, password, server || ctx.siteConfig.dnsServerIp); + const testResult = await dns.refresh(username, password, server || siteConfig.dnsServerIp); if (!testResult.success) { - return ctx.errorResponse(res, 401, `Invalid credentials: ${testResult.error}`); + return errorResponse(res, `Invalid credentials: ${testResult.error}`, 401); } - const dnsServer = server || ctx.siteConfig.dnsServerIp; - await ctx.credentialManager.store('dns.username', username, { type: 'dns', server: dnsServer }); - await ctx.credentialManager.store('dns.password', password, { type: 'dns', server: dnsServer }); - await ctx.credentialManager.store('dns.server', dnsServer, { type: 'dns' }); - ctx.log.info('dns', 'DNS credentials saved to credential manager (encrypted)'); + const dnsServer = server || siteConfig.dnsServerIp; + await credentialManager.store('dns.username', username, { type: 'dns', server: dnsServer }); + await credentialManager.store('dns.password', password, { type: 'dns', server: dnsServer }); + await credentialManager.store('dns.server', dnsServer, { type: 'dns' }); + log.info('dns', 'DNS credentials saved to credential manager (encrypted)'); - res.json({ - success: true, + success(res, { message: 'DNS credentials saved and verified (encrypted)', - tokenExpiry: ctx.dns.getTokenExpiry() + tokenExpiry: dns.getTokenExpiry() }); }, 'dns-credentials')); // DELETE /credentials — Delete stored DNS credentials - router.delete('/credentials', ctx.asyncHandler(async (req, res) => { + router.delete('/credentials', asyncHandler(async (req, res) => { // Delete global credentials - await ctx.credentialManager.delete('dns.username'); - await ctx.credentialManager.delete('dns.password'); - await ctx.credentialManager.delete('dns.server'); + await credentialManager.delete('dns.username'); + await credentialManager.delete('dns.password'); + await credentialManager.delete('dns.server'); // Delete per-server credentials (both old flat and new typed format) - for (const dnsId of Object.keys(ctx.siteConfig.dnsServers || {})) { - await ctx.credentialManager.delete(`dns.${dnsId}.username`); - await ctx.credentialManager.delete(`dns.${dnsId}.password`); + for (const dnsId of Object.keys(siteConfig.dnsServers || {})) { + await credentialManager.delete(`dns.${dnsId}.username`); + await credentialManager.delete(`dns.${dnsId}.password`); for (const role of ['readonly', 'admin']) { - await ctx.credentialManager.delete(`dns.${dnsId}.${role}.username`); - await ctx.credentialManager.delete(`dns.${dnsId}.${role}.password`); + await credentialManager.delete(`dns.${dnsId}.${role}.username`); + await credentialManager.delete(`dns.${dnsId}.${role}.password`); } } - if (await exists(ctx.dns.credentialsFile)) { - await fsp.unlink(ctx.dns.credentialsFile); + if (await exists(dns.credentialsFile)) { + await fsp.unlink(dns.credentialsFile); } - ctx.dns.setToken(''); - ctx.dns.setTokenExpiry(null); - ctx.log.info('dns', 'DNS credentials deleted from credential manager'); - res.json({ success: true, message: 'DNS credentials removed' }); + dns.setToken(''); + dns.setTokenExpiry(null); + log.info('dns', 'DNS credentials deleted from credential manager'); + success(res, { message: 'DNS credentials removed' }); }, 'dns-credentials-delete')); // POST /restart/:dnsId — Restart a DNS server (proxied through backend for auth) - router.post('/restart/:dnsId', ctx.asyncHandler(async (req, res) => { + router.post('/restart/:dnsId', asyncHandler(async (req, res) => { const { dnsId } = req.params; - const serverInfo = ctx.siteConfig.dnsServers?.[dnsId]; + const serverInfo = siteConfig.dnsServers?.[dnsId]; if (!serverInfo?.ip) { - return ctx.errorResponse(res, 400, `Unknown DNS server: ${dnsId}`); + return errorResponse(res, `Unknown DNS server: ${dnsId}`, 400); } - const tokenResult = await ctx.dns.getTokenForServer(serverInfo.ip, 'admin'); + const tokenResult = await dns.getTokenForServer(serverInfo.ip, 'admin'); if (!tokenResult.success) { - return ctx.errorResponse(res, 401, 'DNS admin authentication failed. Ensure admin credentials are configured.'); + return errorResponse(res, 'DNS admin authentication failed. Ensure admin credentials are configured.', 401); } - const dnsPort = ctx.siteConfig.dnsServerPort || '5380'; + const dnsPort = siteConfig.dnsServerPort || '5380'; try { const url = `http://${serverInfo.ip}:${dnsPort}/api/admin/restart?token=${encodeURIComponent(tokenResult.token)}`; - const response = await ctx.fetchT(url, { method: 'POST', timeout: 5000 }); + const response = await fetchT(url, { method: 'POST', timeout: 5000 }); const result = await response.json(); if (result.status === 'ok') { - res.json({ success: true, message: 'Restart initiated' }); + success(res, { message: 'Restart initiated' }); } else { - ctx.errorResponse(res, 500, result.errorMessage || 'Restart failed'); + errorResponse(res, result.errorMessage || 'Restart failed', 500); } } catch (err) { // Connection drop is expected during restart - res.json({ success: true, message: 'Restart initiated (connection closed)' }); + success(res, { message: 'Restart initiated (connection closed)' }); } }, 'dns-restart')); // POST /refresh-token — Force refresh DNS token - router.post('/refresh-token', ctx.asyncHandler(async (req, res) => { - const result = await ctx.dns.ensureToken(); + router.post('/refresh-token', asyncHandler(async (req, res) => { + const result = await dns.ensureToken(); if (result.success) { - res.json({ - success: true, + success(res, { message: 'Token refreshed successfully', - tokenExpiry: ctx.dns.getTokenExpiry() + tokenExpiry: dns.getTokenExpiry() }); } else { - ctx.errorResponse(res, 401, result.error); + errorResponse(res, result.error, 401); } }, 'dns-refresh-token')); // GET /check-update — Check for Technitium DNS server updates - router.get('/check-update', ctx.asyncHandler(async (req, res) => { + router.get('/check-update', asyncHandler(async (req, res) => { try { const { server } = req.query; if (!server) { - return ctx.errorResponse(res, 400, 'Server IP required'); + return errorResponse(res, 'Server IP required', 400); } const serverIp = validateDnsServer(server); if (!serverIp) { - return ctx.errorResponse(res, 400, 'Server must be a configured DNS server'); + return errorResponse(res, 'Server must be a configured DNS server', 400); } // Authenticate with admin credentials for update check - const tokenResult = await ctx.dns.getTokenForServer(serverIp, 'admin'); + const tokenResult = await dns.getTokenForServer(serverIp, 'admin'); if (!tokenResult.success) { - return ctx.errorResponse(res, 401, 'DNS authentication failed. Ensure credentials are configured.'); + return errorResponse(res, 'DNS authentication failed. Ensure credentials are configured.', 401); } - const dnsPort = ctx.siteConfig.dnsServerPort || '5380'; + const dnsPort = siteConfig.dnsServerPort || '5380'; const url = `http://${serverIp}:${dnsPort}/api/user/checkForUpdate?token=${encodeURIComponent(tokenResult.token)}`; - ctx.log.info('dns', 'Checking DNS update', { server }); + log.info('dns', 'Checking DNS update', { server }); - const response = await ctx.fetchT(url, { + const response = await fetchT(url, { method: 'GET', headers: { 'Accept': 'application/json', @@ -536,14 +551,13 @@ module.exports = function(ctx) { const text = await response.text(); if (!text || text.trim() === '') { - return ctx.errorResponse(res, 500, 'Empty response from DNS server'); + return errorResponse(res, 'Empty response from DNS server', 500); } const result = JSON.parse(text); if (result.status === 'ok') { - res.json({ - success: true, + success(res, { updateAvailable: result.response.updateAvailable, currentVersion: result.response.currentVersion, updateVersion: result.response.updateVersion || null, @@ -553,55 +567,54 @@ module.exports = function(ctx) { instructionsLink: result.response.instructionsLink || null }); } else { - ctx.errorResponse(res, 500, result.errorMessage || 'Check failed'); + errorResponse(res, result.errorMessage || 'Check failed', 500); } } catch (error) { - ctx.log.error('dns', 'DNS update check error', { error: error.message }); - ctx.errorResponse(res, 500, ctx.safeErrorMessage(error)); + log.error('dns', 'DNS update check error', { error: error.message }); + errorResponse(res, safeErrorMessage(error), 500); } }, 'dns-check-update')); // POST /update — Update Technitium DNS server // Note: Technitium v14+ has no installUpdate API. This endpoint checks for updates // and returns download info. The frontend handles showing update instructions. - router.post('/update', ctx.asyncHandler(async (req, res) => { + router.post('/update', asyncHandler(async (req, res) => { try { const { server } = req.query; if (!server) { - return ctx.errorResponse(res, 400, 'Server IP required'); + return errorResponse(res, 'Server IP required', 400); } const serverIp = validateDnsServer(server); if (!serverIp) { - return ctx.errorResponse(res, 400, 'Server must be a configured DNS server'); + return errorResponse(res, 'Server must be a configured DNS server', 400); } // Authenticate with admin credentials for update operations - const tokenResult = await ctx.dns.getTokenForServer(serverIp, 'admin'); + const tokenResult = await dns.getTokenForServer(serverIp, 'admin'); if (!tokenResult.success) { - return ctx.errorResponse(res, 401, 'DNS authentication failed. Ensure credentials are configured.'); + return errorResponse(res, 'DNS authentication failed. Ensure credentials are configured.', 401); } - const dnsPort = ctx.siteConfig.dnsServerPort || '5380'; + const dnsPort = siteConfig.dnsServerPort || '5380'; // Check if update is available - const checkResponse = await ctx.fetchT( + const checkResponse = await fetchT( `http://${serverIp}:${dnsPort}/api/user/checkForUpdate?token=${encodeURIComponent(tokenResult.token)}`, { method: 'GET', headers: { 'Accept': 'application/json' } } ); const checkText = await checkResponse.text(); if (!checkText || checkText.trim() === '') { - return ctx.errorResponse(res, 500, 'Empty response from DNS server during check'); + return errorResponse(res, 'Empty response from DNS server during check', 500); } const checkResult = JSON.parse(checkText); if (checkResult.status !== 'ok') { - return ctx.errorResponse(res, 500, checkResult.errorMessage || 'Update check failed'); + return errorResponse(res, checkResult.errorMessage || 'Update check failed', 500); } if (!checkResult.response.updateAvailable) { - return res.json({ - success: true, + return success(res, { message: 'Already up to date', currentVersion: checkResult.response.currentVersion, updated: false @@ -610,10 +623,9 @@ module.exports = function(ctx) { // Technitium v14+ does not have an installUpdate API endpoint. // Return the update info with download link so the frontend can guide the user. - ctx.log.info('dns', 'Update available for DNS server', { server, currentVersion: checkResult.response.currentVersion, updateVersion: checkResult.response.updateVersion }); + log.info('dns', 'Update available for DNS server', { server, currentVersion: checkResult.response.currentVersion, updateVersion: checkResult.response.updateVersion }); - res.json({ - success: true, + success(res, { message: `Update available: ${checkResult.response.updateVersion}`, previousVersion: checkResult.response.currentVersion, newVersion: checkResult.response.updateVersion, @@ -623,8 +635,8 @@ module.exports = function(ctx) { manualUpdateRequired: true }); } catch (error) { - ctx.log.error('dns', 'DNS update error', { error: error.message }); - ctx.errorResponse(res, 500, ctx.safeErrorMessage(error)); + log.error('dns', 'DNS update error', { error: error.message }); + errorResponse(res, safeErrorMessage(error), 500); } }, 'dns-update')); diff --git a/dashcaddy-api/server.js b/dashcaddy-api/server.js index 17fc33d..5608379 100644 --- a/dashcaddy-api/server.js +++ b/dashcaddy-api/server.js @@ -1185,7 +1185,15 @@ Object.assign(ctx, { const apiRouter = express.Router(); apiRouter.use(authRoutes(ctx)); apiRouter.use(configRoutes(ctx)); -apiRouter.use('/dns', dnsRoutes(ctx)); +apiRouter.use('/dns', dnsRoutes({ + dns: ctx.dns, + siteConfig: ctx.siteConfig, + asyncHandler: ctx.asyncHandler, + log: ctx.log, + safeErrorMessage: ctx.safeErrorMessage, + fetchT: ctx.fetchT, + credentialManager: ctx.credentialManager +})); apiRouter.use('/notifications', notificationRoutes(ctx)); apiRouter.use('/containers', containerRoutes({ docker: ctx.docker,