diff --git a/dashcaddy-api/middleware.js b/dashcaddy-api/middleware.js index 73ff999..60d0588 100644 --- a/dashcaddy-api/middleware.js +++ b/dashcaddy-api/middleware.js @@ -293,6 +293,8 @@ module.exports = function configureMiddleware(app, { { path: '/api/logo', exact: true, method: 'GET' }, { path: '/api/favicon', exact: true, method: 'GET' }, { path: '/api/themes', exact: true, method: 'GET' }, + { path: '/api/license/status', exact: true, method: 'GET' }, + { path: '/api/license/feature/', prefix: true, method: 'GET' }, ]; function isPublicRoute(req) { diff --git a/dashcaddy-api/startup-validator.js b/dashcaddy-api/startup-validator.js index 74ec0e1..11c3e57 100644 --- a/dashcaddy-api/startup-validator.js +++ b/dashcaddy-api/startup-validator.js @@ -162,14 +162,12 @@ async function validateStartupConfig({ log, CADDYFILE_PATH, SERVICES_FILE, CONFI async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP }) { try { const topCardServices = [ - { id: 'dns1', name: 'DNS1' }, - { id: 'dns2', name: 'DNS2' }, { id: 'internet', name: 'Internet' }, ]; - // Add dns3 if it exists in dnsServers config - if (siteConfig?.dnsServers?.dns3) { - topCardServices.push({ id: 'dns3', name: 'DNS3' }); + // Dynamically add all configured DNS servers from config + for (const [id, info] of Object.entries(siteConfig?.dnsServers || {})) { + topCardServices.push({ id, name: info.name || id.toUpperCase() }); } let appServices = []; diff --git a/status/js/core/credentials.js b/status/js/core/credentials.js index 0838573..c17ba4b 100644 --- a/status/js/core/credentials.js +++ b/status/js/core/credentials.js @@ -1,87 +1,17 @@ // ========== CREDENTIAL MANAGEMENT ========== (function () { - // Inject the token-management modal HTML + // Inject skeleton modal — credential sections are built dynamically when opened injectModal('token-management-modal', `
-

🔑 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.

- -
-

DNS1 (Windows)

-
-
- - -
- - -
-
-
- - -
- - -
-
-
-
-
- - -
-

DNS2 (Linux)

-
-
- - -
- - -
-
-
- - -
- - -
-
-
-
-
- - -
-

DNS3 (AlmaLinux)

-
-
- - -
- - -
-
-
- - -
- - -
-
-
-
-
+
`); - // 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() {