-
🔑 DNS Credentials
+
\u{1F511} DNS Credentials
Enter Technitium DNS login credentials. Read-only accounts are used for logs; admin accounts for restarts, records, and updates.
-
-
-
-
-
-
-
-
+
`);
- // Credential encryption — key stored in sessionStorage (not localStorage) so encrypted
- // values in localStorage can't be decrypted without the current session key.
- // On session close, key is lost; credentials are re-synced from backend on next save.
+ // ── Helpers ──
+
+ function getDnsIds() {
+ return Object.keys(SITE.dnsServers || {});
+ }
+
+ function getDnsDisplayName(dnsId) {
+ const server = (SITE.dnsServers || {})[dnsId];
+ return server?.name || dnsId.toUpperCase();
+ }
+
+ /** Build per-server credential form sections from SITE.dnsServers */
+ function buildCredentialSections() {
+ const container = document.getElementById('dns-cred-sections');
+ if (!container) return;
+ container.innerHTML = '';
+
+ const dnsIds = getDnsIds();
+ if (dnsIds.length === 0) {
+ container.innerHTML = '
No DNS servers configured.
';
+ return;
+ }
+
+ for (const id of dnsIds) {
+ container.insertAdjacentHTML('beforeend', `
+
+
${getDnsDisplayName(id)}
+
+
+
+ `);
+ }
+ }
+
+ // ── Credential encryption ──
+
function getEncryptionKey() {
- // 1. Check sessionStorage first (current session)
let key = safeSessionGet('dashcaddy-encryption-key');
if (key) return key;
- // 2. Migrate from localStorage if old key exists (one-time upgrade)
const oldKey = safeGet('dashcaddy-encryption-key');
if (oldKey) {
safeSessionSet('dashcaddy-encryption-key', oldKey);
- safeRemove('dashcaddy-encryption-key'); // Remove from localStorage
+ safeRemove('dashcaddy-encryption-key');
return oldKey;
}
- // 3. Generate new key for this session
const array = new Uint8Array(32);
crypto.getRandomValues(array);
key = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
@@ -117,34 +94,27 @@
}
const ENCRYPTION_KEY = getEncryptionKey();
- // AES-like multi-round encryption with per-value IV (stronger than single-pass XOR)
function credentialEncrypt(text, key) {
if (!text) return '';
- // Generate random IV (8 bytes)
const iv = crypto.getRandomValues(new Uint8Array(8));
const ivHex = Array.from(iv, b => b.toString(16).padStart(2, '0')).join('');
- // Derive round key from key + IV for uniqueness per value
const keyBytes = new TextEncoder().encode(key + ivHex);
let result = '';
for (let i = 0; i < text.length; i++) {
- // Multi-source XOR: key byte + IV byte + position-dependent mixing
const charCode = text.charCodeAt(i)
^ keyBytes[i % keyBytes.length]
^ iv[i % iv.length]
^ ((i * 31 + 17) & 0xFF);
result += String.fromCharCode(charCode);
}
- // Prepend IV to ciphertext so we can decrypt later
return ivHex + ':' + btoa(result);
}
function credentialDecrypt(encryptedText, key) {
if (!encryptedText) return '';
try {
- // Check for IV prefix (new format: "ivhex:base64")
const colonIdx = encryptedText.indexOf(':');
if (colonIdx === 16) {
- // New format with IV
const ivHex = encryptedText.substring(0, 16);
const iv = new Uint8Array(ivHex.match(/.{2}/g).map(h => parseInt(h, 16)));
const decoded = atob(encryptedText.substring(17));
@@ -159,7 +129,6 @@
}
return result;
}
- // Legacy format (plain XOR base64) — for migration
const decoded = atob(encryptedText);
let result = '';
for (let i = 0; i < decoded.length; i++) {
@@ -172,7 +141,8 @@
}
}
- // Credential storage functions
+ // ── Credential storage ──
+
function getCredential(dnsId, tokenType, credType) {
const encrypted = safeGet(`${dnsId}-${tokenType}-${credType}-enc`);
return credentialDecrypt(encrypted, ENCRYPTION_KEY);
@@ -204,24 +174,18 @@
}
function getAllCredentials() {
- return {
- dns1: {
- readonly: { username: getUsername('dns1', 'readonly'), token: getToken('dns1', 'readonly') },
- admin: { username: getUsername('dns1', 'admin'), token: getToken('dns1', 'admin') }
- },
- dns2: {
- readonly: { username: getUsername('dns2', 'readonly'), token: getToken('dns2', 'readonly') },
- admin: { username: getUsername('dns2', 'admin'), token: getToken('dns2', 'admin') }
- },
- dns3: {
- readonly: { username: getUsername('dns3', 'readonly'), token: getToken('dns3', 'readonly') },
- admin: { username: getUsername('dns3', 'admin'), token: getToken('dns3', 'admin') }
- }
- };
+ const result = {};
+ for (const dnsId of getDnsIds()) {
+ result[dnsId] = {
+ readonly: { username: getUsername(dnsId, 'readonly'), token: getToken(dnsId, 'readonly') },
+ admin: { username: getUsername(dnsId, 'admin'), token: getToken(dnsId, 'admin') }
+ };
+ }
+ return result;
}
function clearAllCredentials() {
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ getDnsIds().forEach(dnsId => {
['readonly', 'admin'].forEach(tokenType => {
['token', 'username'].forEach(credType => {
safeRemove(`${dnsId}-${tokenType}-${credType}-enc`);
@@ -250,26 +214,30 @@
};
}
- // Token Management Modal handlers
+ // ── Modal handlers ──
+
+ // Open modal — build sections dynamically, populate from stored creds
document.getElementById('manage-tokens')?.addEventListener('click', () => {
+ buildCredentialSections();
const modal = document.getElementById('token-management-modal');
const creds = getAllCredentials();
- // Populate fields with existing credentials
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
- document.getElementById(`${dnsId}-readonly-username`).value = creds[dnsId].readonly.username;
- document.getElementById(`${dnsId}-readonly-token`).value = creds[dnsId].readonly.token;
- document.getElementById(`${dnsId}-admin-username`).value = creds[dnsId].admin.username;
- document.getElementById(`${dnsId}-admin-token`).value = creds[dnsId].admin.token;
+ getDnsIds().forEach(dnsId => {
+ const c = creds[dnsId];
+ document.getElementById(`${dnsId}-readonly-username`).value = c.readonly.username;
+ document.getElementById(`${dnsId}-readonly-token`).value = c.readonly.token;
+ document.getElementById(`${dnsId}-admin-username`).value = c.admin.username;
+ document.getElementById(`${dnsId}-admin-token`).value = c.admin.token;
document.getElementById(`${dnsId}-token-status`).textContent = '';
});
modal.classList.add('show');
});
- // Toggle password visibility
- document.querySelectorAll('.token-toggle').forEach(btn => {
- btn.addEventListener('click', () => {
+ // Toggle password visibility + backdrop close (event delegation for dynamic elements)
+ document.getElementById('token-management-modal')?.addEventListener('click', (e) => {
+ const btn = e.target.closest('.token-toggle');
+ if (btn) {
const targetId = btn.dataset.target;
const input = document.getElementById(targetId);
if (input.type === 'password') {
@@ -279,12 +247,19 @@
input.type = 'password';
btn.textContent = '\u{1F441}';
}
- });
+ return;
+ }
+ if (e.target.id === 'token-management-modal') {
+ e.target.classList.remove('show');
+ }
});
+ // Save credentials
document.getElementById('token-save')?.addEventListener('click', async () => {
- // Save all credentials to localStorage
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ const dnsIds = getDnsIds();
+
+ // Save all to localStorage
+ dnsIds.forEach(dnsId => {
setUsername(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-username`).value.trim());
setToken(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-token`).value.trim());
setUsername(dnsId, 'admin', document.getElementById(`${dnsId}-admin-username`).value.trim());
@@ -295,7 +270,7 @@
const servers = {};
let hasAnyCreds = false;
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ dnsIds.forEach(dnsId => {
const entry = {};
const roUser = document.getElementById(`${dnsId}-readonly-username`).value.trim();
const roPass = document.getElementById(`${dnsId}-readonly-token`).value.trim();
@@ -316,8 +291,7 @@
});
if (hasAnyCreds) {
- // Show syncing status
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ dnsIds.forEach(dnsId => {
if (servers[dnsId]) {
document.getElementById(`${dnsId}-token-status`).textContent = 'Verifying...';
document.getElementById(`${dnsId}-token-status`).className = 'token-status';
@@ -333,7 +307,7 @@
const data = await res.json();
if (data.results) {
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ dnsIds.forEach(dnsId => {
const statusEl = document.getElementById(`${dnsId}-token-status`);
if (!servers[dnsId]) { statusEl.textContent = ''; return; }
const result = data.results[dnsId];
@@ -349,14 +323,14 @@
}
});
} else if (data.success) {
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ dnsIds.forEach(dnsId => {
if (servers[dnsId]) {
document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved';
document.getElementById(`${dnsId}-token-status`).className = 'token-status success';
}
});
} else {
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ dnsIds.forEach(dnsId => {
if (servers[dnsId]) {
document.getElementById(`${dnsId}-token-status`).textContent = '\u2717 ' + (data.error || 'Failed');
document.getElementById(`${dnsId}-token-status`).className = 'token-status error';
@@ -365,7 +339,7 @@
}
} catch (e) {
console.error('Failed to sync DNS credentials to backend:', e);
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ dnsIds.forEach(dnsId => {
if (servers[dnsId]) {
document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved locally (sync failed)';
document.getElementById(`${dnsId}-token-status`).className = 'token-status';
@@ -373,15 +347,15 @@
});
}
} else {
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ dnsIds.forEach(dnsId => {
document.getElementById(`${dnsId}-token-status`).textContent = '';
});
}
// Auto-close after delay if all succeeded
setTimeout(() => {
- const allGood = ['dns1', 'dns2', 'dns3'].every(dnsId => {
- const status = document.getElementById(`${dnsId}-token-status`).textContent;
+ const allGood = dnsIds.every(dnsId => {
+ const status = document.getElementById(`${dnsId}-token-status`)?.textContent;
return !status || status.includes('\u2713');
});
if (allGood) closeModal('token-management-modal');
@@ -395,7 +369,7 @@
document.getElementById('token-clear-all')?.addEventListener('click', async () => {
if (confirm('Clear all stored DNS credentials? This cannot be undone.')) {
clearAllCredentials();
- ['dns1', 'dns2', 'dns3'].forEach(dnsId => {
+ getDnsIds().forEach(dnsId => {
document.getElementById(`${dnsId}-readonly-username`).value = '';
document.getElementById(`${dnsId}-readonly-token`).value = '';
document.getElementById(`${dnsId}-admin-username`).value = '';
@@ -409,14 +383,7 @@
}
});
- // Close modal on backdrop click
- document.getElementById('token-management-modal')?.addEventListener('click', (e) => {
- if (e.target.id === 'token-management-modal') {
- e.target.classList.remove('show');
- }
- });
-
- // Window exports
+ // ── Window exports ──
window.getToken = getToken;
window.getUsername = getUsername;
window.setToken = setToken;
@@ -425,5 +392,7 @@
window.getCredential = getCredential;
window.setCredential = setCredential;
window.getEncryptionKey = getEncryptionKey;
+ window.getDnsIds = getDnsIds;
+ window.getDnsDisplayName = getDnsDisplayName;
})();
diff --git a/status/js/core/service-create.js b/status/js/core/service-create.js
index 86b4419..08ad3e1 100644
--- a/status/js/core/service-create.js
+++ b/status/js/core/service-create.js
@@ -284,7 +284,7 @@
const results = { dns: null, caddy: null, dashboard: false };
if (createDns) {
- const adminToken = window.getToken('dns2', 'admin');
+ const adminToken = window.getToken(getPrimaryDnsId(), 'admin');
if (adminToken) {
try {
const dnsResponse = await secureFetch('/api/v1/dns/record', {
@@ -457,7 +457,7 @@
const healthCheck = document.getElementById('health-check-input')?.value || '';
const timeout = document.getElementById('timeout-input')?.value || 30;
- const dnsToken = window.getToken('dns2', 'admin');
+ const dnsToken = window.getToken(getPrimaryDnsId(), 'admin');
if (!name || !port || !ip) {
showNotification('Please fill in Name, Port, and IP Address', 'warning');
diff --git a/status/js/core/service-infrastructure.js b/status/js/core/service-infrastructure.js
index 04c3c73..86379af 100644
--- a/status/js/core/service-infrastructure.js
+++ b/status/js/core/service-infrastructure.js
@@ -146,7 +146,7 @@
// ===== CREATE DNS RECORD =====
async function createDnsRecord(subdomain, ip, ttl = DC.DEFAULTS.TTL) {
- const dnsToken = window.getToken('dns2', 'admin');
+ const dnsToken = window.getToken(getPrimaryDnsId(), 'admin');
if (!dnsToken) {
throw new Error('DNS admin token not configured. Please set it in the Tokens menu.');
diff --git a/status/js/globals.js b/status/js/globals.js
index d7a6cee..7dad7b0 100644
--- a/status/js/globals.js
+++ b/status/js/globals.js
@@ -85,6 +85,14 @@ function getDnsServerAddr(dnsId) {
const s = SITE.dnsServers[dnsId];
return s ? `${s.ip}:${s.port}` : buildDomain(dnsId);
}
+/** Get the DNS ID whose IP matches the primary DNS config (dns.ip) */
+function getPrimaryDnsId() {
+ if (!SITE.dnsIp) return null;
+ for (const [id, info] of Object.entries(SITE.dnsServers)) {
+ if (info.ip === SITE.dnsIp) return id;
+ }
+ return null;
+}
// ===== DYNAMIC DNS CARD RENDERER =====
function renderDnsCards() {