Make DNS servers fully dynamic from config.json
DNS server IDs (dns1, dns2, dns3) were hardcoded throughout the frontend and backend. Now config.json's dnsServers object is the single source of truth — adding or removing a DNS server in config automatically updates the dashboard cards, credential modal, health checks, and probes. - credentials.js: rebuild modal sections dynamically from SITE.dnsServers - globals.js: add getPrimaryDnsId() helper for primary DNS lookups - service-create.js, service-infrastructure.js: use dynamic DNS ID - startup-validator.js: dynamic topCardServices from config - middleware.js: add license endpoints to public routes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2815233e86
commit
f2f33b4b40
6 files changed
+121
-144
No files matched your search
@@ -293,6 +293,8 @@ module.exports = function configureMiddleware(app, {
|
|||||||
{ path: '/api/logo', exact: true, method: 'GET' },
|
{ path: '/api/logo', exact: true, method: 'GET' },
|
||||||
{ path: '/api/favicon', exact: true, method: 'GET' },
|
{ path: '/api/favicon', exact: true, method: 'GET' },
|
||||||
{ path: '/api/themes', 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) {
|
function isPublicRoute(req) {
|
||||||
|
|||||||
@@ -162,14 +162,12 @@ async function validateStartupConfig({ log, CADDYFILE_PATH, SERVICES_FILE, CONFI
|
|||||||
async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP }) {
|
async function syncHealthCheckerServices({ log, SERVICES_FILE, servicesStateManager, healthChecker, buildServiceUrl, siteConfig, APP }) {
|
||||||
try {
|
try {
|
||||||
const topCardServices = [
|
const topCardServices = [
|
||||||
{ id: 'dns1', name: 'DNS1' },
|
|
||||||
{ id: 'dns2', name: 'DNS2' },
|
|
||||||
{ id: 'internet', name: 'Internet' },
|
{ id: 'internet', name: 'Internet' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add dns3 if it exists in dnsServers config
|
// Dynamically add all configured DNS servers from config
|
||||||
if (siteConfig?.dnsServers?.dns3) {
|
for (const [id, info] of Object.entries(siteConfig?.dnsServers || {})) {
|
||||||
topCardServices.push({ id: 'dns3', name: 'DNS3' });
|
topCardServices.push({ id, name: info.name || id.toUpperCase() });
|
||||||
}
|
}
|
||||||
|
|
||||||
let appServices = [];
|
let appServices = [];
|
||||||
|
|||||||
+105
-136
@@ -1,87 +1,17 @@
|
|||||||
// ========== CREDENTIAL MANAGEMENT ==========
|
// ========== CREDENTIAL MANAGEMENT ==========
|
||||||
(function () {
|
(function () {
|
||||||
|
|
||||||
// Inject the token-management modal HTML
|
// Inject skeleton modal — credential sections are built dynamically when opened
|
||||||
injectModal('token-management-modal', `
|
injectModal('token-management-modal', `
|
||||||
<div id="token-management-modal" class="weather-modal">
|
<div id="token-management-modal" class="weather-modal">
|
||||||
<div class="weather-modal-content" style="min-width: 500px; max-width: 600px;">
|
<div class="weather-modal-content" style="min-width: 500px; max-width: 600px;">
|
||||||
<h3>🔑 DNS Credentials</h3>
|
<h3>\u{1F511} DNS Credentials</h3>
|
||||||
|
|
||||||
<p style="font-size: 0.85rem; color: var(--muted); margin: 0 0 16px;">
|
<p style="font-size: 0.85rem; color: var(--muted); margin: 0 0 16px;">
|
||||||
Enter Technitium DNS login credentials. Read-only accounts are used for logs; admin accounts for restarts, records, and updates.
|
Enter Technitium DNS login credentials. Read-only accounts are used for logs; admin accounts for restarts, records, and updates.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- DNS1 Credentials -->
|
<div id="dns-cred-sections"></div>
|
||||||
<div class="token-section">
|
|
||||||
<h4 class="token-section-title">DNS1 (Windows)</h4>
|
|
||||||
<div class="token-grid">
|
|
||||||
<div class="token-field">
|
|
||||||
<label for="dns1-readonly-username">\u{1F4D6} Read-Only (Logs):</label>
|
|
||||||
<input type="text" id="dns1-readonly-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
|
||||||
<div class="token-input-row">
|
|
||||||
<input type="password" id="dns1-readonly-token" placeholder="Password" autocomplete="off" />
|
|
||||||
<button type="button" class="token-toggle" data-target="dns1-readonly-token">\u{1F441}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="token-field">
|
|
||||||
<label for="dns1-admin-username">\u{1F527} Admin:</label>
|
|
||||||
<input type="text" id="dns1-admin-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
|
||||||
<div class="token-input-row">
|
|
||||||
<input type="password" id="dns1-admin-token" placeholder="Password" autocomplete="off" />
|
|
||||||
<button type="button" class="token-toggle" data-target="dns1-admin-token">\u{1F441}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="token-status" id="dns1-token-status"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DNS2 Credentials -->
|
|
||||||
<div class="token-section">
|
|
||||||
<h4 class="token-section-title">DNS2 (Linux)</h4>
|
|
||||||
<div class="token-grid">
|
|
||||||
<div class="token-field">
|
|
||||||
<label for="dns2-readonly-username">\u{1F4D6} Read-Only (Logs):</label>
|
|
||||||
<input type="text" id="dns2-readonly-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
|
||||||
<div class="token-input-row">
|
|
||||||
<input type="password" id="dns2-readonly-token" placeholder="Password" autocomplete="off" />
|
|
||||||
<button type="button" class="token-toggle" data-target="dns2-readonly-token">\u{1F441}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="token-field">
|
|
||||||
<label for="dns2-admin-username">\u{1F527} Admin:</label>
|
|
||||||
<input type="text" id="dns2-admin-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
|
||||||
<div class="token-input-row">
|
|
||||||
<input type="password" id="dns2-admin-token" placeholder="Password" autocomplete="off" />
|
|
||||||
<button type="button" class="token-toggle" data-target="dns2-admin-token">\u{1F441}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="token-status" id="dns2-token-status"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DNS3 Credentials -->
|
|
||||||
<div class="token-section">
|
|
||||||
<h4 class="token-section-title">DNS3 (AlmaLinux)</h4>
|
|
||||||
<div class="token-grid">
|
|
||||||
<div class="token-field">
|
|
||||||
<label for="dns3-readonly-username">\u{1F4D6} Read-Only (Logs):</label>
|
|
||||||
<input type="text" id="dns3-readonly-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
|
||||||
<div class="token-input-row">
|
|
||||||
<input type="password" id="dns3-readonly-token" placeholder="Password" autocomplete="off" />
|
|
||||||
<button type="button" class="token-toggle" data-target="dns3-readonly-token">\u{1F441}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="token-field">
|
|
||||||
<label for="dns3-admin-username">\u{1F527} Admin:</label>
|
|
||||||
<input type="text" id="dns3-admin-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
|
||||||
<div class="token-input-row">
|
|
||||||
<input type="password" id="dns3-admin-token" placeholder="Password" autocomplete="off" />
|
|
||||||
<button type="button" class="token-toggle" data-target="dns3-admin-token">\u{1F441}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="token-status" id="dns3-token-status"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="weather-modal-buttons modal-footer-bar">
|
<div class="weather-modal-buttons modal-footer-bar">
|
||||||
<button id="token-clear-all" style="margin-right: auto; background: color-mix(in srgb, var(--bad-fg) 15%, transparent); border-color: var(--bad-fg); color: var(--bad-fg);">Clear All</button>
|
<button id="token-clear-all" style="margin-right: auto; background: color-mix(in srgb, var(--bad-fg) 15%, transparent); border-color: var(--bad-fg); color: var(--bad-fg);">Clear All</button>
|
||||||
@@ -92,23 +22,70 @@
|
|||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Credential encryption — key stored in sessionStorage (not localStorage) so encrypted
|
// ── Helpers ──
|
||||||
// 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.
|
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 = '<p style="color: var(--muted); text-align: center; padding: 20px;">No DNS servers configured.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of dnsIds) {
|
||||||
|
container.insertAdjacentHTML('beforeend', `
|
||||||
|
<div class="token-section">
|
||||||
|
<h4 class="token-section-title">${getDnsDisplayName(id)}</h4>
|
||||||
|
<div class="token-grid">
|
||||||
|
<div class="token-field">
|
||||||
|
<label for="${id}-readonly-username">\u{1F4D6} Read-Only (Logs):</label>
|
||||||
|
<input type="text" id="${id}-readonly-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
||||||
|
<div class="token-input-row">
|
||||||
|
<input type="password" id="${id}-readonly-token" placeholder="Password" autocomplete="off" />
|
||||||
|
<button type="button" class="token-toggle" data-target="${id}-readonly-token">\u{1F441}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="token-field">
|
||||||
|
<label for="${id}-admin-username">\u{1F527} Admin:</label>
|
||||||
|
<input type="text" id="${id}-admin-username" placeholder="Username" autocomplete="off" style="margin-bottom: 4px;" />
|
||||||
|
<div class="token-input-row">
|
||||||
|
<input type="password" id="${id}-admin-token" placeholder="Password" autocomplete="off" />
|
||||||
|
<button type="button" class="token-toggle" data-target="${id}-admin-token">\u{1F441}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="token-status" id="${id}-token-status"></div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Credential encryption ──
|
||||||
|
|
||||||
function getEncryptionKey() {
|
function getEncryptionKey() {
|
||||||
// 1. Check sessionStorage first (current session)
|
|
||||||
let key = safeSessionGet('dashcaddy-encryption-key');
|
let key = safeSessionGet('dashcaddy-encryption-key');
|
||||||
if (key) return key;
|
if (key) return key;
|
||||||
|
|
||||||
// 2. Migrate from localStorage if old key exists (one-time upgrade)
|
|
||||||
const oldKey = safeGet('dashcaddy-encryption-key');
|
const oldKey = safeGet('dashcaddy-encryption-key');
|
||||||
if (oldKey) {
|
if (oldKey) {
|
||||||
safeSessionSet('dashcaddy-encryption-key', oldKey);
|
safeSessionSet('dashcaddy-encryption-key', oldKey);
|
||||||
safeRemove('dashcaddy-encryption-key'); // Remove from localStorage
|
safeRemove('dashcaddy-encryption-key');
|
||||||
return oldKey;
|
return oldKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Generate new key for this session
|
|
||||||
const array = new Uint8Array(32);
|
const array = new Uint8Array(32);
|
||||||
crypto.getRandomValues(array);
|
crypto.getRandomValues(array);
|
||||||
key = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
key = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
||||||
@@ -117,34 +94,27 @@
|
|||||||
}
|
}
|
||||||
const ENCRYPTION_KEY = getEncryptionKey();
|
const ENCRYPTION_KEY = getEncryptionKey();
|
||||||
|
|
||||||
// AES-like multi-round encryption with per-value IV (stronger than single-pass XOR)
|
|
||||||
function credentialEncrypt(text, key) {
|
function credentialEncrypt(text, key) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
// Generate random IV (8 bytes)
|
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(8));
|
const iv = crypto.getRandomValues(new Uint8Array(8));
|
||||||
const ivHex = Array.from(iv, b => b.toString(16).padStart(2, '0')).join('');
|
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);
|
const keyBytes = new TextEncoder().encode(key + ivHex);
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
// Multi-source XOR: key byte + IV byte + position-dependent mixing
|
|
||||||
const charCode = text.charCodeAt(i)
|
const charCode = text.charCodeAt(i)
|
||||||
^ keyBytes[i % keyBytes.length]
|
^ keyBytes[i % keyBytes.length]
|
||||||
^ iv[i % iv.length]
|
^ iv[i % iv.length]
|
||||||
^ ((i * 31 + 17) & 0xFF);
|
^ ((i * 31 + 17) & 0xFF);
|
||||||
result += String.fromCharCode(charCode);
|
result += String.fromCharCode(charCode);
|
||||||
}
|
}
|
||||||
// Prepend IV to ciphertext so we can decrypt later
|
|
||||||
return ivHex + ':' + btoa(result);
|
return ivHex + ':' + btoa(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
function credentialDecrypt(encryptedText, key) {
|
function credentialDecrypt(encryptedText, key) {
|
||||||
if (!encryptedText) return '';
|
if (!encryptedText) return '';
|
||||||
try {
|
try {
|
||||||
// Check for IV prefix (new format: "ivhex:base64")
|
|
||||||
const colonIdx = encryptedText.indexOf(':');
|
const colonIdx = encryptedText.indexOf(':');
|
||||||
if (colonIdx === 16) {
|
if (colonIdx === 16) {
|
||||||
// New format with IV
|
|
||||||
const ivHex = encryptedText.substring(0, 16);
|
const ivHex = encryptedText.substring(0, 16);
|
||||||
const iv = new Uint8Array(ivHex.match(/.{2}/g).map(h => parseInt(h, 16)));
|
const iv = new Uint8Array(ivHex.match(/.{2}/g).map(h => parseInt(h, 16)));
|
||||||
const decoded = atob(encryptedText.substring(17));
|
const decoded = atob(encryptedText.substring(17));
|
||||||
@@ -159,7 +129,6 @@
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// Legacy format (plain XOR base64) — for migration
|
|
||||||
const decoded = atob(encryptedText);
|
const decoded = atob(encryptedText);
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let i = 0; i < decoded.length; i++) {
|
for (let i = 0; i < decoded.length; i++) {
|
||||||
@@ -172,7 +141,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credential storage functions
|
// ── Credential storage ──
|
||||||
|
|
||||||
function getCredential(dnsId, tokenType, credType) {
|
function getCredential(dnsId, tokenType, credType) {
|
||||||
const encrypted = safeGet(`${dnsId}-${tokenType}-${credType}-enc`);
|
const encrypted = safeGet(`${dnsId}-${tokenType}-${credType}-enc`);
|
||||||
return credentialDecrypt(encrypted, ENCRYPTION_KEY);
|
return credentialDecrypt(encrypted, ENCRYPTION_KEY);
|
||||||
@@ -204,24 +174,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAllCredentials() {
|
function getAllCredentials() {
|
||||||
return {
|
const result = {};
|
||||||
dns1: {
|
for (const dnsId of getDnsIds()) {
|
||||||
readonly: { username: getUsername('dns1', 'readonly'), token: getToken('dns1', 'readonly') },
|
result[dnsId] = {
|
||||||
admin: { username: getUsername('dns1', 'admin'), token: getToken('dns1', 'admin') }
|
readonly: { username: getUsername(dnsId, 'readonly'), token: getToken(dnsId, 'readonly') },
|
||||||
},
|
admin: { username: getUsername(dnsId, 'admin'), token: getToken(dnsId, 'admin') }
|
||||||
dns2: {
|
};
|
||||||
readonly: { username: getUsername('dns2', 'readonly'), token: getToken('dns2', 'readonly') },
|
}
|
||||||
admin: { username: getUsername('dns2', 'admin'), token: getToken('dns2', 'admin') }
|
return result;
|
||||||
},
|
|
||||||
dns3: {
|
|
||||||
readonly: { username: getUsername('dns3', 'readonly'), token: getToken('dns3', 'readonly') },
|
|
||||||
admin: { username: getUsername('dns3', 'admin'), token: getToken('dns3', 'admin') }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAllCredentials() {
|
function clearAllCredentials() {
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
getDnsIds().forEach(dnsId => {
|
||||||
['readonly', 'admin'].forEach(tokenType => {
|
['readonly', 'admin'].forEach(tokenType => {
|
||||||
['token', 'username'].forEach(credType => {
|
['token', 'username'].forEach(credType => {
|
||||||
safeRemove(`${dnsId}-${tokenType}-${credType}-enc`);
|
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', () => {
|
document.getElementById('manage-tokens')?.addEventListener('click', () => {
|
||||||
|
buildCredentialSections();
|
||||||
const modal = document.getElementById('token-management-modal');
|
const modal = document.getElementById('token-management-modal');
|
||||||
const creds = getAllCredentials();
|
const creds = getAllCredentials();
|
||||||
|
|
||||||
// Populate fields with existing credentials
|
getDnsIds().forEach(dnsId => {
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
const c = creds[dnsId];
|
||||||
document.getElementById(`${dnsId}-readonly-username`).value = creds[dnsId].readonly.username;
|
document.getElementById(`${dnsId}-readonly-username`).value = c.readonly.username;
|
||||||
document.getElementById(`${dnsId}-readonly-token`).value = creds[dnsId].readonly.token;
|
document.getElementById(`${dnsId}-readonly-token`).value = c.readonly.token;
|
||||||
document.getElementById(`${dnsId}-admin-username`).value = creds[dnsId].admin.username;
|
document.getElementById(`${dnsId}-admin-username`).value = c.admin.username;
|
||||||
document.getElementById(`${dnsId}-admin-token`).value = creds[dnsId].admin.token;
|
document.getElementById(`${dnsId}-admin-token`).value = c.admin.token;
|
||||||
document.getElementById(`${dnsId}-token-status`).textContent = '';
|
document.getElementById(`${dnsId}-token-status`).textContent = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.classList.add('show');
|
modal.classList.add('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle password visibility
|
// Toggle password visibility + backdrop close (event delegation for dynamic elements)
|
||||||
document.querySelectorAll('.token-toggle').forEach(btn => {
|
document.getElementById('token-management-modal')?.addEventListener('click', (e) => {
|
||||||
btn.addEventListener('click', () => {
|
const btn = e.target.closest('.token-toggle');
|
||||||
|
if (btn) {
|
||||||
const targetId = btn.dataset.target;
|
const targetId = btn.dataset.target;
|
||||||
const input = document.getElementById(targetId);
|
const input = document.getElementById(targetId);
|
||||||
if (input.type === 'password') {
|
if (input.type === 'password') {
|
||||||
@@ -279,12 +247,19 @@
|
|||||||
input.type = 'password';
|
input.type = 'password';
|
||||||
btn.textContent = '\u{1F441}';
|
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 () => {
|
document.getElementById('token-save')?.addEventListener('click', async () => {
|
||||||
// Save all credentials to localStorage
|
const dnsIds = getDnsIds();
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
|
||||||
|
// Save all to localStorage
|
||||||
|
dnsIds.forEach(dnsId => {
|
||||||
setUsername(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-username`).value.trim());
|
setUsername(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-username`).value.trim());
|
||||||
setToken(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-token`).value.trim());
|
setToken(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-token`).value.trim());
|
||||||
setUsername(dnsId, 'admin', document.getElementById(`${dnsId}-admin-username`).value.trim());
|
setUsername(dnsId, 'admin', document.getElementById(`${dnsId}-admin-username`).value.trim());
|
||||||
@@ -295,7 +270,7 @@
|
|||||||
const servers = {};
|
const servers = {};
|
||||||
let hasAnyCreds = false;
|
let hasAnyCreds = false;
|
||||||
|
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
dnsIds.forEach(dnsId => {
|
||||||
const entry = {};
|
const entry = {};
|
||||||
const roUser = document.getElementById(`${dnsId}-readonly-username`).value.trim();
|
const roUser = document.getElementById(`${dnsId}-readonly-username`).value.trim();
|
||||||
const roPass = document.getElementById(`${dnsId}-readonly-token`).value.trim();
|
const roPass = document.getElementById(`${dnsId}-readonly-token`).value.trim();
|
||||||
@@ -316,8 +291,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (hasAnyCreds) {
|
if (hasAnyCreds) {
|
||||||
// Show syncing status
|
dnsIds.forEach(dnsId => {
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
|
||||||
if (servers[dnsId]) {
|
if (servers[dnsId]) {
|
||||||
document.getElementById(`${dnsId}-token-status`).textContent = 'Verifying...';
|
document.getElementById(`${dnsId}-token-status`).textContent = 'Verifying...';
|
||||||
document.getElementById(`${dnsId}-token-status`).className = 'token-status';
|
document.getElementById(`${dnsId}-token-status`).className = 'token-status';
|
||||||
@@ -333,7 +307,7 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.results) {
|
if (data.results) {
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
dnsIds.forEach(dnsId => {
|
||||||
const statusEl = document.getElementById(`${dnsId}-token-status`);
|
const statusEl = document.getElementById(`${dnsId}-token-status`);
|
||||||
if (!servers[dnsId]) { statusEl.textContent = ''; return; }
|
if (!servers[dnsId]) { statusEl.textContent = ''; return; }
|
||||||
const result = data.results[dnsId];
|
const result = data.results[dnsId];
|
||||||
@@ -349,14 +323,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (data.success) {
|
} else if (data.success) {
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
dnsIds.forEach(dnsId => {
|
||||||
if (servers[dnsId]) {
|
if (servers[dnsId]) {
|
||||||
document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved';
|
document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved';
|
||||||
document.getElementById(`${dnsId}-token-status`).className = 'token-status success';
|
document.getElementById(`${dnsId}-token-status`).className = 'token-status success';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
dnsIds.forEach(dnsId => {
|
||||||
if (servers[dnsId]) {
|
if (servers[dnsId]) {
|
||||||
document.getElementById(`${dnsId}-token-status`).textContent = '\u2717 ' + (data.error || 'Failed');
|
document.getElementById(`${dnsId}-token-status`).textContent = '\u2717 ' + (data.error || 'Failed');
|
||||||
document.getElementById(`${dnsId}-token-status`).className = 'token-status error';
|
document.getElementById(`${dnsId}-token-status`).className = 'token-status error';
|
||||||
@@ -365,7 +339,7 @@
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to sync DNS credentials to backend:', e);
|
console.error('Failed to sync DNS credentials to backend:', e);
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
dnsIds.forEach(dnsId => {
|
||||||
if (servers[dnsId]) {
|
if (servers[dnsId]) {
|
||||||
document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved locally (sync failed)';
|
document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved locally (sync failed)';
|
||||||
document.getElementById(`${dnsId}-token-status`).className = 'token-status';
|
document.getElementById(`${dnsId}-token-status`).className = 'token-status';
|
||||||
@@ -373,15 +347,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
dnsIds.forEach(dnsId => {
|
||||||
document.getElementById(`${dnsId}-token-status`).textContent = '';
|
document.getElementById(`${dnsId}-token-status`).textContent = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-close after delay if all succeeded
|
// Auto-close after delay if all succeeded
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const allGood = ['dns1', 'dns2', 'dns3'].every(dnsId => {
|
const allGood = dnsIds.every(dnsId => {
|
||||||
const status = document.getElementById(`${dnsId}-token-status`).textContent;
|
const status = document.getElementById(`${dnsId}-token-status`)?.textContent;
|
||||||
return !status || status.includes('\u2713');
|
return !status || status.includes('\u2713');
|
||||||
});
|
});
|
||||||
if (allGood) closeModal('token-management-modal');
|
if (allGood) closeModal('token-management-modal');
|
||||||
@@ -395,7 +369,7 @@
|
|||||||
document.getElementById('token-clear-all')?.addEventListener('click', async () => {
|
document.getElementById('token-clear-all')?.addEventListener('click', async () => {
|
||||||
if (confirm('Clear all stored DNS credentials? This cannot be undone.')) {
|
if (confirm('Clear all stored DNS credentials? This cannot be undone.')) {
|
||||||
clearAllCredentials();
|
clearAllCredentials();
|
||||||
['dns1', 'dns2', 'dns3'].forEach(dnsId => {
|
getDnsIds().forEach(dnsId => {
|
||||||
document.getElementById(`${dnsId}-readonly-username`).value = '';
|
document.getElementById(`${dnsId}-readonly-username`).value = '';
|
||||||
document.getElementById(`${dnsId}-readonly-token`).value = '';
|
document.getElementById(`${dnsId}-readonly-token`).value = '';
|
||||||
document.getElementById(`${dnsId}-admin-username`).value = '';
|
document.getElementById(`${dnsId}-admin-username`).value = '';
|
||||||
@@ -409,14 +383,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close modal on backdrop click
|
// ── Window exports ──
|
||||||
document.getElementById('token-management-modal')?.addEventListener('click', (e) => {
|
|
||||||
if (e.target.id === 'token-management-modal') {
|
|
||||||
e.target.classList.remove('show');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Window exports
|
|
||||||
window.getToken = getToken;
|
window.getToken = getToken;
|
||||||
window.getUsername = getUsername;
|
window.getUsername = getUsername;
|
||||||
window.setToken = setToken;
|
window.setToken = setToken;
|
||||||
@@ -425,5 +392,7 @@
|
|||||||
window.getCredential = getCredential;
|
window.getCredential = getCredential;
|
||||||
window.setCredential = setCredential;
|
window.setCredential = setCredential;
|
||||||
window.getEncryptionKey = getEncryptionKey;
|
window.getEncryptionKey = getEncryptionKey;
|
||||||
|
window.getDnsIds = getDnsIds;
|
||||||
|
window.getDnsDisplayName = getDnsDisplayName;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
const results = { dns: null, caddy: null, dashboard: false };
|
const results = { dns: null, caddy: null, dashboard: false };
|
||||||
|
|
||||||
if (createDns) {
|
if (createDns) {
|
||||||
const adminToken = window.getToken('dns2', 'admin');
|
const adminToken = window.getToken(getPrimaryDnsId(), 'admin');
|
||||||
if (adminToken) {
|
if (adminToken) {
|
||||||
try {
|
try {
|
||||||
const dnsResponse = await secureFetch('/api/v1/dns/record', {
|
const dnsResponse = await secureFetch('/api/v1/dns/record', {
|
||||||
@@ -457,7 +457,7 @@
|
|||||||
const healthCheck = document.getElementById('health-check-input')?.value || '';
|
const healthCheck = document.getElementById('health-check-input')?.value || '';
|
||||||
const timeout = document.getElementById('timeout-input')?.value || 30;
|
const timeout = document.getElementById('timeout-input')?.value || 30;
|
||||||
|
|
||||||
const dnsToken = window.getToken('dns2', 'admin');
|
const dnsToken = window.getToken(getPrimaryDnsId(), 'admin');
|
||||||
|
|
||||||
if (!name || !port || !ip) {
|
if (!name || !port || !ip) {
|
||||||
showNotification('Please fill in Name, Port, and IP Address', 'warning');
|
showNotification('Please fill in Name, Port, and IP Address', 'warning');
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
// ===== CREATE DNS RECORD =====
|
// ===== CREATE DNS RECORD =====
|
||||||
|
|
||||||
async function createDnsRecord(subdomain, ip, ttl = DC.DEFAULTS.TTL) {
|
async function createDnsRecord(subdomain, ip, ttl = DC.DEFAULTS.TTL) {
|
||||||
const dnsToken = window.getToken('dns2', 'admin');
|
const dnsToken = window.getToken(getPrimaryDnsId(), 'admin');
|
||||||
|
|
||||||
if (!dnsToken) {
|
if (!dnsToken) {
|
||||||
throw new Error('DNS admin token not configured. Please set it in the Tokens menu.');
|
throw new Error('DNS admin token not configured. Please set it in the Tokens menu.');
|
||||||
|
|||||||
@@ -85,6 +85,14 @@ function getDnsServerAddr(dnsId) {
|
|||||||
const s = SITE.dnsServers[dnsId];
|
const s = SITE.dnsServers[dnsId];
|
||||||
return s ? `${s.ip}:${s.port}` : buildDomain(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 =====
|
// ===== DYNAMIC DNS CARD RENDERER =====
|
||||||
function renderDnsCards() {
|
function renderDnsCards() {
|
||||||
|
|||||||
Reference in new issue
Block a user