// ========== CREDENTIAL MANAGEMENT ========== (function () { // Inject skeleton modal — credential sections are built dynamically when opened injectModal('token-management-modal', `

\u{1F511} DNS Credentials

Enter Technitium DNS login credentials. Read-only accounts are used for logs; admin accounts for restarts, records, and updates.

`); // ── 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() { let key = safeSessionGet('dashcaddy-encryption-key'); if (key) return key; const oldKey = safeGet('dashcaddy-encryption-key'); if (oldKey) { safeSessionSet('dashcaddy-encryption-key', oldKey); safeRemove('dashcaddy-encryption-key'); return oldKey; } const array = new Uint8Array(32); crypto.getRandomValues(array); key = Array.from(array, b => b.toString(16).padStart(2, '0')).join(''); safeSessionSet('dashcaddy-encryption-key', key); return key; } const ENCRYPTION_KEY = getEncryptionKey(); function credentialEncrypt(text, key) { if (!text) return ''; const iv = crypto.getRandomValues(new Uint8Array(8)); const ivHex = Array.from(iv, b => b.toString(16).padStart(2, '0')).join(''); const keyBytes = new TextEncoder().encode(key + ivHex); let result = ''; for (let i = 0; i < text.length; i++) { const charCode = text.charCodeAt(i) ^ keyBytes[i % keyBytes.length] ^ iv[i % iv.length] ^ ((i * 31 + 17) & 0xFF); result += String.fromCharCode(charCode); } return ivHex + ':' + btoa(result); } function credentialDecrypt(encryptedText, key) { if (!encryptedText) return ''; try { const colonIdx = encryptedText.indexOf(':'); if (colonIdx === 16) { 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)); const keyBytes = new TextEncoder().encode(key + ivHex); let result = ''; for (let i = 0; i < decoded.length; i++) { const charCode = decoded.charCodeAt(i) ^ keyBytes[i % keyBytes.length] ^ iv[i % iv.length] ^ ((i * 31 + 17) & 0xFF); result += String.fromCharCode(charCode); } return result; } const decoded = atob(encryptedText); let result = ''; for (let i = 0; i < decoded.length; i++) { const charCode = decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length); result += String.fromCharCode(charCode); } return result; } catch (e) { return ''; } } // ── Credential storage ── function getCredential(dnsId, tokenType, credType) { const encrypted = safeGet(`${dnsId}-${tokenType}-${credType}-enc`); return credentialDecrypt(encrypted, ENCRYPTION_KEY); } function setCredential(dnsId, tokenType, credType, value) { const key = `${dnsId}-${tokenType}-${credType}-enc`; if (value) { safeSet(key, credentialEncrypt(value, ENCRYPTION_KEY)); } else { safeRemove(key); } } function getToken(dnsId, tokenType) { return getCredential(dnsId, tokenType, 'token'); } function getUsername(dnsId, tokenType) { return getCredential(dnsId, tokenType, 'username'); } function setToken(dnsId, tokenType, token) { setCredential(dnsId, tokenType, 'token', token); } function setUsername(dnsId, tokenType, username) { setCredential(dnsId, tokenType, 'username', username); } function getAllCredentials() { 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() { getDnsIds().forEach(dnsId => { ['readonly', 'admin'].forEach(tokenType => { ['token', 'username'].forEach(credType => { safeRemove(`${dnsId}-${tokenType}-${credType}-enc`); }); }); safeRemove(`${dnsId}-token-enc`); safeRemove(`${dnsId}-username-enc`); }); } function getStoredCredentials(dnsId) { const readonlyToken = getToken(dnsId, 'readonly'); const readonlyUsername = getUsername(dnsId, 'readonly'); const adminToken = getToken(dnsId, 'admin'); const adminUsername = getUsername(dnsId, 'admin'); const oldToken = credentialDecrypt(safeGet(`${dnsId}-token-enc`), ENCRYPTION_KEY); const oldUsername = credentialDecrypt(safeGet(`${dnsId}-username-enc`), ENCRYPTION_KEY); return { username: adminUsername || readonlyUsername || oldUsername, token: adminToken || readonlyToken || oldToken, readonlyToken: readonlyToken || oldToken, readonlyUsername: readonlyUsername || oldUsername, adminToken: adminToken || oldToken, adminUsername: adminUsername || oldUsername }; } // ── 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(); 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 + 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') { input.type = 'text'; btn.textContent = '\u{1F648}'; } else { 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 () => { 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()); setToken(dnsId, 'admin', document.getElementById(`${dnsId}-admin-token`).value.trim()); }); // Build per-server credentials payload for backend sync const servers = {}; let hasAnyCreds = false; dnsIds.forEach(dnsId => { const entry = {}; const roUser = document.getElementById(`${dnsId}-readonly-username`).value.trim(); const roPass = document.getElementById(`${dnsId}-readonly-token`).value.trim(); const adminUser = document.getElementById(`${dnsId}-admin-username`).value.trim(); const adminPass = document.getElementById(`${dnsId}-admin-token`).value.trim(); if (roUser && roPass) { entry.readonly = { username: roUser, password: roPass }; hasAnyCreds = true; } if (adminUser && adminPass) { entry.admin = { username: adminUser, password: adminPass }; hasAnyCreds = true; } if (Object.keys(entry).length > 0) { servers[dnsId] = entry; } }); if (hasAnyCreds) { dnsIds.forEach(dnsId => { if (servers[dnsId]) { document.getElementById(`${dnsId}-token-status`).textContent = 'Verifying...'; document.getElementById(`${dnsId}-token-status`).className = 'token-status'; } }); try { const res = await secureFetch('/api/v1/dns/credentials', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ servers }) }); const data = await res.json(); if (data.results) { dnsIds.forEach(dnsId => { const statusEl = document.getElementById(`${dnsId}-token-status`); if (!servers[dnsId]) { statusEl.textContent = ''; return; } const result = data.results[dnsId]; if (result?.success) { statusEl.textContent = '\u2713 Verified & saved'; statusEl.className = 'token-status success'; } else if (result?.partial) { statusEl.textContent = '\u2713 ' + result.partial; statusEl.className = 'token-status success'; } else { statusEl.textContent = '\u2717 ' + (result?.error || 'Login failed'); statusEl.className = 'token-status error'; } }); } else if (data.success) { dnsIds.forEach(dnsId => { if (servers[dnsId]) { document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved'; document.getElementById(`${dnsId}-token-status`).className = 'token-status success'; } }); } else { 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'; } }); } } catch (e) { console.error('Failed to sync DNS credentials to backend:', e); 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'; } }); } } else { dnsIds.forEach(dnsId => { document.getElementById(`${dnsId}-token-status`).textContent = ''; }); } // Auto-close after delay if all succeeded setTimeout(() => { const allGood = dnsIds.every(dnsId => { const status = document.getElementById(`${dnsId}-token-status`)?.textContent; return !status || status.includes('\u2713'); }); if (allGood) closeModal('token-management-modal'); }, 1500); }); document.getElementById('token-cancel')?.addEventListener('click', () => { closeModal('token-management-modal'); }); document.getElementById('token-clear-all')?.addEventListener('click', async () => { if (confirm('Clear all stored DNS credentials? This cannot be undone.')) { clearAllCredentials(); getDnsIds().forEach(dnsId => { document.getElementById(`${dnsId}-readonly-username`).value = ''; document.getElementById(`${dnsId}-readonly-token`).value = ''; document.getElementById(`${dnsId}-admin-username`).value = ''; document.getElementById(`${dnsId}-admin-token`).value = ''; document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Cleared'; document.getElementById(`${dnsId}-token-status`).className = 'token-status success'; }); try { await secureFetch('/api/v1/dns/credentials', { method: 'DELETE' }); } catch (_) {} } }); // ── Window exports ── window.getToken = getToken; window.getUsername = getUsername; window.setToken = setToken; window.setUsername = setUsername; window.getAllCredentials = getAllCredentials; window.getCredential = getCredential; window.setCredential = setCredential; window.getEncryptionKey = getEncryptionKey; window.getDnsIds = getDnsIds; window.getDnsDisplayName = getDnsDisplayName; })();