Merge krystie-improvements into main
Resolves 24 conflicts between Hermes (DC-008/009/010 + response-helper
envelope standardization) and Krystie (DC-005 src/ refactor path fixes,
DC-006 TOTP integration, DC-007 new test suites, cloud backup
destinations).
Conflict resolutions:
- src/utils/logging.js: took ours (consumers depend on logError/
safeErrorMessage/createLogger exports)
- src/config/site.js: merged (her factored validateAndLogConfig +
applyConfigFields helpers)
- src/context/dns.js: took hers (admin/readonly role iteration for
write operations)
- src/utilities/backup-
manager.js: took hers (Dropbox/WebDAV/SFTP cloud feature)
- status/dist/*, status/
sw.js: took hers (minified bundles + newer SW cache)
Additional fix (post-merge regression):
- src/monitoring/health-checker.js: fixed DC-005 path miss —
'require(./platform-paths)' → 'require(../../platform-paths)'
Test status: 921/922 passing. One known failure in logging.test.js
(async file-handle timing) tracked as follow-up.
This commit is contained in:
+236
-17
@@ -2,10 +2,10 @@ const express = require('express');
|
||||
const fs = require('fs');
|
||||
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');
|
||||
const { ValidationError, AuthenticationError, NotFoundError } = require('../errors');
|
||||
const { APP, TIMEOUTS, CADDY, DNS_RECORD_TYPES, REGEX, SESSION_TTL } = require('../src/utilities/constants');
|
||||
const { exists } = require('../src/utilities/fs-helpers');
|
||||
const { success, error: errorResponse } = require('../src/utils/responses');
|
||||
const { ValidationError, AuthenticationError, NotFoundError } = require('../src/utilities/errors');
|
||||
|
||||
/**
|
||||
* DNS routes factory
|
||||
@@ -26,7 +26,8 @@ module.exports = function({
|
||||
log,
|
||||
safeErrorMessage,
|
||||
fetchT,
|
||||
credentialManager
|
||||
credentialManager,
|
||||
dnsPropagationChecker
|
||||
}) {
|
||||
const router = express.Router();
|
||||
|
||||
@@ -41,7 +42,137 @@ module.exports = function({
|
||||
return serverIp;
|
||||
}
|
||||
|
||||
// DELETE /record — Delete a DNS record from Technitium
|
||||
// ===== DNS PROVIDER ENDPOINTS =====
|
||||
|
||||
// GET /providers — List all available DNS providers
|
||||
router.get('/providers', asyncHandler(async (req, res) => {
|
||||
const providers = dns.getAvailableProviders ? dns.getAvailableProviders() : [];
|
||||
const activeProvider = dns.getProviderId ? dns.getProviderId() : 'technitium';
|
||||
success(res, { providers, activeProvider });
|
||||
}, 'dns-providers-list'));
|
||||
|
||||
// GET /provider/status — Get active provider status
|
||||
router.get('/provider/status', asyncHandler(async (req, res) => {
|
||||
if (!dns.getActiveProvider) {
|
||||
return success(res, { providerId: 'technitium', capabilities: ['create-record', 'delete-record', 'resolve', 'list-records', 'logs', 'restart', 'update-check', 'credentials', 'zones'] });
|
||||
}
|
||||
try {
|
||||
const provider = dns.getActiveProvider();
|
||||
const status = await provider.getStatus();
|
||||
success(res, status);
|
||||
} catch (err) {
|
||||
errorResponse(res, safeErrorMessage(err), 500);
|
||||
}
|
||||
}, 'dns-provider-status'));
|
||||
|
||||
// ===== UNIVERSAL RECORD ENDPOINTS (work with any provider) =====
|
||||
|
||||
// POST /universal/record — Create a DNS record via any provider
|
||||
router.post('/universal/record', asyncHandler(async (req, res) => {
|
||||
if (!dns.getActiveProvider) {
|
||||
// Fallback to legacy Technitium route
|
||||
return res.redirect(307, '/api/dns/record');
|
||||
}
|
||||
const { domain, ip, ttl, type, server } = req.body;
|
||||
if (!domain || !ip) throw new ValidationError('domain and ip are required');
|
||||
if (!REGEX.DOMAIN.test(domain)) throw new ValidationError('[DC-301] Invalid domain format');
|
||||
if (!validatorLib.isIP(ip)) throw new ValidationError('[DC-210] Invalid IP address');
|
||||
|
||||
try {
|
||||
const provider = dns.getActiveProvider();
|
||||
if (!provider.supportsCapability('create-record')) {
|
||||
const result = await provider.createRecord({
|
||||
domain, zone: siteConfig.tld?.replace(/^\./, '') || '',
|
||||
type: type || 'A', value: ip, ttl: ttl || 300, overwrite: true
|
||||
});
|
||||
return success(res, {
|
||||
message: result.message || `DNS record instructions provided`,
|
||||
manual: true,
|
||||
instructions: result.instructions
|
||||
});
|
||||
}
|
||||
|
||||
const result = await provider.createRecord({
|
||||
domain, zone: siteConfig.tld?.replace(/^\./, '') || '',
|
||||
type: type || 'A', value: ip, ttl: ttl || 300, overwrite: true
|
||||
});
|
||||
|
||||
// Start propagation check in background
|
||||
if (dnsPropagationChecker && ip) {
|
||||
dnsPropagationChecker.startVerification(domain, ip).catch(err => {
|
||||
log('DNS propagation check start failed:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
success(res, {
|
||||
message: result.status === 'manual' ? result.message : `DNS record ${domain} -> ${ip} created`,
|
||||
provider: dns.getProviderId(),
|
||||
...(result.instructions ? { manual: true, instructions: result.instructions } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('dns', 'Universal DNS record creation error', { error: error.message });
|
||||
errorResponse(res, safeErrorMessage(error), 500);
|
||||
}
|
||||
}, 'dns-universal-create'));
|
||||
|
||||
// DELETE /universal/record — Delete a DNS record via any provider
|
||||
router.delete('/universal/record', asyncHandler(async (req, res) => {
|
||||
if (!dns.getActiveProvider) {
|
||||
return res.redirect(307, '/api/dns/record');
|
||||
}
|
||||
const { domain, type, value } = req.query;
|
||||
if (!domain) throw new ValidationError('domain is required');
|
||||
if (!REGEX.DOMAIN.test(domain)) throw new ValidationError('[DC-301] Invalid domain format');
|
||||
|
||||
try {
|
||||
const provider = dns.getActiveProvider();
|
||||
const result = await provider.deleteRecord({
|
||||
domain, type: type || 'A', value
|
||||
});
|
||||
|
||||
success(res, {
|
||||
message: result.status === 'manual' ? result.message : `DNS record ${domain} deleted`,
|
||||
provider: dns.getProviderId(),
|
||||
...(result.instructions ? { manual: true, instructions: result.instructions } : {})
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('dns', 'Universal DNS record deletion error', { error: error.message });
|
||||
errorResponse(res, safeErrorMessage(error), 500);
|
||||
}
|
||||
}, 'dns-universal-delete'));
|
||||
|
||||
// GET /universal/resolve — Resolve a domain via any provider
|
||||
router.get('/universal/resolve', asyncHandler(async (req, res) => {
|
||||
if (!dns.getActiveProvider) {
|
||||
return res.redirect(307, '/api/dns/resolve');
|
||||
}
|
||||
const { domain, type } = req.query;
|
||||
if (!domain) throw new ValidationError('domain is required');
|
||||
if (!REGEX.DOMAIN.test(domain)) throw new ValidationError('[DC-301] Invalid domain format');
|
||||
|
||||
try {
|
||||
const provider = dns.getActiveProvider();
|
||||
const result = await provider.resolveRecords({
|
||||
domain, zone: siteConfig.tld?.replace(/^\./, '') || '',
|
||||
type: type || 'A'
|
||||
});
|
||||
|
||||
if (result.response?.records?.length > 0) {
|
||||
const ipAddresses = result.response.records
|
||||
.filter(r => r.type === (type || 'A'))
|
||||
.map(r => r.rData?.ipAddress || r.content || r.rData?.address)
|
||||
.filter(Boolean);
|
||||
success(res, { answer: ipAddresses });
|
||||
} else {
|
||||
throw new NotFoundError('No records found for domain');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('dns', 'Universal DNS resolve error', { error: error.message });
|
||||
errorResponse(res, safeErrorMessage(error), error.statusCode || 500);
|
||||
}
|
||||
}, 'dns-universal-resolve'));
|
||||
|
||||
// ===== LEGACY TECHNITIUM-SPECIFIC ROUTES (unchanged) =====
|
||||
router.delete('/record', asyncHandler(async (req, res) => {
|
||||
const { domain, type, token, server, ipAddress } = req.query;
|
||||
|
||||
@@ -139,6 +270,14 @@ module.exports = function({
|
||||
});
|
||||
|
||||
if (result.status === 'ok') {
|
||||
// Start DNS propagation verification in background
|
||||
if (dnsPropagationChecker && ip) {
|
||||
const fullDomain = domain;
|
||||
dnsPropagationChecker.startVerification(fullDomain, ip).catch(err => {
|
||||
log('DNS propagation check start failed:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
success(res, { message: `DNS record ${domain} -> ${ip} created` });
|
||||
} else {
|
||||
// Error handled by middleware
|
||||
@@ -194,8 +333,13 @@ module.exports = function({
|
||||
}
|
||||
}, 'dns-resolve'));
|
||||
|
||||
// GET /logs — Fetch DNS query logs from Technitium
|
||||
// GET /logs — Fetch DNS query logs (Technitium only)
|
||||
router.get('/logs', asyncHandler(async (req, res) => {
|
||||
// Capability gate: logs are provider-specific
|
||||
if (dns.supportsCapability && !dns.supportsCapability('logs')) {
|
||||
return success(res, { server: 'N/A', count: 0, logs: [], message: 'DNS logs not supported by current provider' });
|
||||
}
|
||||
|
||||
const { server, limit } = req.query;
|
||||
|
||||
if (!server) {
|
||||
@@ -239,9 +383,8 @@ module.exports = function({
|
||||
|
||||
const response = await fetchT(technitiumUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'text/plain' },
|
||||
timeout: 10000
|
||||
});
|
||||
headers: { 'Accept': 'text/plain' }
|
||||
}, 10000);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
@@ -409,7 +552,7 @@ module.exports = function({
|
||||
}
|
||||
}
|
||||
|
||||
return success(res, {
|
||||
return ok(res, {
|
||||
message: anySuccess ? 'Credentials saved for one or more servers' : 'All server credential tests failed',
|
||||
results
|
||||
});
|
||||
@@ -474,8 +617,13 @@ module.exports = function({
|
||||
success(res, { message: 'DNS credentials removed' });
|
||||
}, 'dns-credentials-delete'));
|
||||
|
||||
// POST /restart/:dnsId — Restart a DNS server (proxied through backend for auth)
|
||||
// POST /restart/:dnsId — Restart a DNS server (Technitium only)
|
||||
router.post('/restart/:dnsId', asyncHandler(async (req, res) => {
|
||||
// Capability gate
|
||||
if (dns.supportsCapability && !dns.supportsCapability('restart')) {
|
||||
return errorResponse(res, 'Server restart not supported by current DNS provider', 501);
|
||||
}
|
||||
|
||||
const { dnsId } = req.params;
|
||||
const serverInfo = siteConfig.dnsServers?.[dnsId];
|
||||
if (!serverInfo?.ip) {
|
||||
@@ -490,7 +638,7 @@ module.exports = function({
|
||||
const dnsPort = siteConfig.dnsServerPort || '5380';
|
||||
try {
|
||||
const url = `http://${serverInfo.ip}:${dnsPort}/api/admin/restart?token=${encodeURIComponent(tokenResult.token)}`;
|
||||
const response = await fetchT(url, { method: 'POST', timeout: 5000 });
|
||||
const response = await fetchT(url, { method: 'POST' }, 5000);
|
||||
const result = await response.json();
|
||||
if (result.status === 'ok') {
|
||||
success(res, { message: 'Restart initiated' });
|
||||
@@ -517,8 +665,13 @@ module.exports = function({
|
||||
}
|
||||
}, 'dns-refresh-token'));
|
||||
|
||||
// GET /check-update — Check for Technitium DNS server updates
|
||||
// GET /check-update — Check for DNS server updates (Technitium only)
|
||||
router.get('/check-update', asyncHandler(async (req, res) => {
|
||||
// Capability gate
|
||||
if (dns.supportsCapability && !dns.supportsCapability('update-check')) {
|
||||
return success(res, { updateAvailable: false, message: 'Update check not supported by current DNS provider' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { server } = req.query;
|
||||
if (!server) {
|
||||
@@ -575,10 +728,13 @@ module.exports = function({
|
||||
}
|
||||
}, '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.
|
||||
// POST /update — Update DNS server (Technitium only)
|
||||
router.post('/update', asyncHandler(async (req, res) => {
|
||||
// Capability gate
|
||||
if (dns.supportsCapability && !dns.supportsCapability('update-check')) {
|
||||
return errorResponse(res, 'Server update not supported by current DNS provider', 501);
|
||||
}
|
||||
|
||||
try {
|
||||
const { server } = req.query;
|
||||
if (!server) {
|
||||
@@ -640,5 +796,68 @@ module.exports = function({
|
||||
}
|
||||
}, 'dns-update'));
|
||||
|
||||
// ===== DNS PROPAGATION =====
|
||||
|
||||
// GET /propagation — Get all recent DNS propagation checks
|
||||
router.get('/propagation', asyncHandler(async (req, res) => {
|
||||
if (!dnsPropagationChecker) {
|
||||
return success(res, { verifications: [], message: 'DNS propagation checker not available' });
|
||||
}
|
||||
|
||||
// Cleanup old entries
|
||||
dnsPropagationChecker.cleanup();
|
||||
|
||||
const verifications = dnsPropagationChecker.getAllVerifications();
|
||||
success(res, { verifications });
|
||||
}, 'dns-propagation-all'));
|
||||
|
||||
// POST /propagation/verify — Manually trigger DNS propagation verification
|
||||
router.post('/propagation/verify', asyncHandler(async (req, res) => {
|
||||
if (!dnsPropagationChecker) {
|
||||
return errorResponse(res, 'DNS propagation checker not available', 503);
|
||||
}
|
||||
|
||||
const { domain, expectedIp } = req.body;
|
||||
|
||||
if (!domain || !expectedIp) {
|
||||
throw new ValidationError('domain and expectedIp are required');
|
||||
}
|
||||
|
||||
// Validate domain format
|
||||
if (!REGEX.DOMAIN.test(domain)) {
|
||||
throw new ValidationError('[DC-301] Invalid domain format');
|
||||
}
|
||||
|
||||
// Validate IP address
|
||||
const validatorLib = require('validator');
|
||||
if (!validatorLib.isIP(expectedIp)) {
|
||||
throw new ValidationError('[DC-210] Invalid IP address');
|
||||
}
|
||||
|
||||
const job = dnsPropagationChecker.startVerification(domain, expectedIp);
|
||||
success(res, {
|
||||
message: 'DNS propagation verification started',
|
||||
domain,
|
||||
expectedIp,
|
||||
status: job.status
|
||||
});
|
||||
}, 'dns-propagation-verify'));
|
||||
|
||||
// GET /propagation/:domain — Get propagation status for a specific domain
|
||||
router.get('/propagation/:domain', asyncHandler(async (req, res) => {
|
||||
if (!dnsPropagationChecker) {
|
||||
return success(res, { verification: null, message: 'DNS propagation checker not available' });
|
||||
}
|
||||
|
||||
const { domain } = req.params;
|
||||
const status = dnsPropagationChecker.getVerificationStatus(domain);
|
||||
|
||||
if (!status) {
|
||||
throw new NotFoundError(`No propagation check found for domain: ${domain}`);
|
||||
}
|
||||
|
||||
success(res, { verification: status });
|
||||
}, 'dns-propagation-domain'));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user