feat: DNS provider abstraction — Technitium, Cloudflare, RFC 2136, Manual
- dns-providers/: adapter base class + registry with auto-discovery - technitium.js: wraps existing Technitium API calls into adapter interface - cloudflare.js: Cloudflare API v4 adapter (zones, records, credentials) - rfc2136.js: RFC 2136 dynamic DNS via nsupdate (BIND, PowerDNS, etc.) - manual.js: no-op adapter for external DNS management with instructions - provider-dns.js: provider-aware DNS context, resolves active adapter from config - Universal helper methods: universalCreateRecord/Delete/ResolveRecord - All 7 route files updated to use universal methods instead of raw dns.call() - Setup wizard: provider dropdown (Technitium, Cloudflare, RFC 2136, Manual) - DNS template selector: added Cloudflare and External/Manual options - Config schema: validates dns.provider field - Capability gating on Technitium-specific endpoints (logs, restart, update) - Backward compatible: no provider set = auto-detect (technitium if dns.ip exists)
This commit is contained in:
+155
-7
@@ -42,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;
|
||||
|
||||
@@ -203,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) {
|
||||
@@ -484,8 +619,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) {
|
||||
@@ -527,8 +667,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) {
|
||||
@@ -585,10 +730,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) {
|
||||
|
||||
Reference in New Issue
Block a user