Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
387
status/js/core/credentials.js
Normal file
387
status/js/core/credentials.js
Normal file
@@ -0,0 +1,387 @@
|
||||
// ========== CREDENTIAL MANAGEMENT ==========
|
||||
(function () {
|
||||
|
||||
// Inject the token-management modal HTML
|
||||
injectModal('token-management-modal', `
|
||||
<div id="token-management-modal" class="weather-modal">
|
||||
<div class="weather-modal-content" style="min-width: 500px; max-width: 600px;">
|
||||
<h3>🔑 DNS Credentials</h3>
|
||||
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<!-- DNS1 Credentials -->
|
||||
<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">
|
||||
<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-cancel">Cancel</button>
|
||||
<button id="token-save" class="btn-accent">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Simple encryption for storing credentials - key is generated per installation
|
||||
function getEncryptionKey() {
|
||||
let key = safeGet('dashcaddy-encryption-key');
|
||||
if (!key) {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
key = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');
|
||||
safeSet('dashcaddy-encryption-key', key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
const ENCRYPTION_KEY = getEncryptionKey();
|
||||
|
||||
function simpleEncrypt(text, key) {
|
||||
if (!text) return '';
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
|
||||
result += String.fromCharCode(charCode);
|
||||
}
|
||||
return btoa(result);
|
||||
}
|
||||
|
||||
function simpleDecrypt(encryptedText, key) {
|
||||
if (!encryptedText) return '';
|
||||
try {
|
||||
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 functions
|
||||
function getCredential(dnsId, tokenType, credType) {
|
||||
const encrypted = safeGet(`${dnsId}-${tokenType}-${credType}-enc`);
|
||||
return simpleDecrypt(encrypted, ENCRYPTION_KEY);
|
||||
}
|
||||
|
||||
function setCredential(dnsId, tokenType, credType, value) {
|
||||
const key = `${dnsId}-${tokenType}-${credType}-enc`;
|
||||
if (value) {
|
||||
safeSet(key, simpleEncrypt(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() {
|
||||
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') }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function clearAllCredentials() {
|
||||
['dns1', 'dns2', 'dns3'].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 = simpleDecrypt(safeGet(`${dnsId}-token-enc`), ENCRYPTION_KEY);
|
||||
const oldUsername = simpleDecrypt(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
|
||||
};
|
||||
}
|
||||
|
||||
// Token Management Modal handlers
|
||||
document.getElementById('manage-tokens')?.addEventListener('click', () => {
|
||||
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;
|
||||
document.getElementById(`${dnsId}-token-status`).textContent = '';
|
||||
});
|
||||
|
||||
modal.classList.add('show');
|
||||
});
|
||||
|
||||
// Toggle password visibility
|
||||
document.querySelectorAll('.token-toggle').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
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}';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('token-save')?.addEventListener('click', async () => {
|
||||
// Save all credentials to localStorage
|
||||
['dns1', 'dns2', 'dns3'].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;
|
||||
|
||||
['dns1', 'dns2', 'dns3'].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) {
|
||||
// Show syncing status
|
||||
['dns1', 'dns2', 'dns3'].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) {
|
||||
['dns1', 'dns2', 'dns3'].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) {
|
||||
['dns1', 'dns2', 'dns3'].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 => {
|
||||
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);
|
||||
['dns1', 'dns2', 'dns3'].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 {
|
||||
['dns1', 'dns2', 'dns3'].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;
|
||||
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();
|
||||
['dns1', 'dns2', 'dns3'].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 (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
// 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.getToken = getToken;
|
||||
window.getUsername = getUsername;
|
||||
window.setToken = setToken;
|
||||
window.setUsername = setUsername;
|
||||
window.getAllCredentials = getAllCredentials;
|
||||
window.getCredential = getCredential;
|
||||
window.setCredential = setCredential;
|
||||
window.getEncryptionKey = getEncryptionKey;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user