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:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

322
status/js/totp-settings.js Normal file
View File

@@ -0,0 +1,322 @@
// ===== TOTP SETTINGS =====
(function() {
injectModal('totp-settings-modal', `<div id="totp-settings-modal" class="weather-modal">
<div class="weather-modal-content" style="min-width: 420px; max-width: 520px;">
<h3 style="margin: 0 0 16px; font-size: 1.1rem;">Authentication Settings</h3>
<!-- Status Banner -->
<div id="totp-status-banner" style="margin-bottom: 16px; padding: 12px 16px; border-radius: 8px; border: 1px solid var(--border); display: flex; align-items: center; gap: 8px;">
<span id="totp-status-dot" class="status-dot"></span>
<span id="totp-status-text" style="font-size: 0.9rem;">TOTP is not configured</span>
</div>
<!-- Setup Button (not configured state) -->
<div id="totp-setup-section">
<button id="totp-setup-btn" style="width: 100%; padding: 12px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem;">
Generate New Secret
</button>
<div style="display: flex; align-items: center; gap: 8px; margin: 10px 0 0;">
<div class="divider-line"></div>
<span class="text-tiny-muted">or</span>
<div class="divider-line"></div>
</div>
<div style="margin-top: 10px;">
<label class="text-hint">Import an existing secret key:</label>
<div style="display: flex; gap: 8px; margin-top: 6px;">
<input type="text" id="totp-import-key" placeholder="Paste your Base32 key" autocomplete="off" spellcheck="false"
style="flex: 1; padding: 10px; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; font-family: monospace; letter-spacing: 1px; text-transform: uppercase;" />
<button id="totp-import-btn" style="padding: 10px 16px; background: var(--card-base); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; white-space: nowrap;">
Import
</button>
</div>
</div>
</div>
<!-- Secret Display (setup flow) -->
<div id="totp-qr-section" style="display: none;">
<!-- Manual Key (primary - for WinAuth/desktop authenticators) -->
<p style="font-size: 0.85rem; color: var(--muted); margin: 0 0 8px;">Copy this key into your authenticator app:</p>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
<code id="totp-manual-key" style="flex: 1; display: block; padding: 12px; background: var(--bg, #0b0f1a); border: 1px solid var(--border); border-radius: 6px; font-size: 1rem; font-family: 'Sami Grotesk', monospace; letter-spacing: 2px; word-break: break-all; user-select: all; color: var(--fg);"></code>
<button id="totp-copy-key" style="padding: 10px 14px; background: var(--card-base); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-size: 1rem; white-space: nowrap; color: var(--fg);" title="Copy to clipboard">📋</button>
</div>
<!-- QR Code (secondary - for mobile apps) -->
<details class="mb-16">
<summary style="cursor: pointer; color: var(--muted); font-size: 0.8rem;">Show QR code (for mobile authenticator apps)</summary>
<div style="text-align: center; margin-top: 8px;">
<img id="totp-qr-image" style="width: 180px; height: 180px; border-radius: 8px;" />
</div>
</details>
<!-- Verify First Code -->
<div style="border-top: 1px solid var(--border); padding-top: 16px;">
<label class="font-bold-sm">Enter code to confirm setup:</label>
<div style="display: flex; gap: 8px; margin-top: 8px;">
<input type="text" id="totp-setup-code" maxlength="6" inputmode="numeric" pattern="[0-9]{6}" placeholder="000000"
style="flex: 1; text-align: center; font-size: 1.3rem; padding: 10px; font-family: 'Sami Grotesk', monospace; letter-spacing: 4px; background: var(--bg); color: var(--fg); border: 2px solid var(--border); border-radius: 8px; outline: none;" />
<button id="totp-confirm-setup" style="padding: 10px 20px; background: var(--accent, #8FD6FF); color: #0b0f1a; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;">
Confirm
</button>
</div>
<div id="totp-setup-error" style="color: var(--bad-fg, #ff9aa3); font-size: 0.8rem; min-height: 1.2em; margin-top: 6px;"></div>
</div>
</div>
<!-- Session Duration (active state) -->
<div id="totp-duration-section" style="display: none; margin-top: 12px;">
<label class="font-bold-sm">Session Duration:</label>
<select id="totp-duration-select" style="width: 100%; padding: 10px; margin-top: 6px; background: var(--bg, #0b0f1a); color: var(--fg); border: 1px solid var(--border); border-radius: 8px; font-size: 0.9rem; cursor: pointer;">
<option value="15m">15 minutes</option>
<option value="30m">30 minutes</option>
<option value="1h">1 hour</option>
<option value="2h">2 hours</option>
<option value="4h">4 hours</option>
<option value="8h">8 hours</option>
<option value="12h">12 hours</option>
<option value="24h">24 hours</option>
<option value="never">Never (disable TOTP)</option>
</select>
<p style="font-size: 0.75rem; color: var(--muted); margin: 4px 0 0;">How long before you need to re-enter your code</p>
</div>
<!-- Disable Button (active state) -->
<div id="totp-disable-section" style="display: none; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);">
<button id="totp-disable-btn" style="width: 100%; padding: 10px; background: transparent; color: var(--bad-fg, #ff9aa3); border: 1px solid var(--bad-fg, #ff9aa3); border-radius: 8px; cursor: pointer; font-size: 0.9rem;">
Disable TOTP
</button>
</div>
<!-- Close -->
<div style="margin-top: 16px; text-align: right;">
<button id="totp-modal-close" style="padding: 8px 20px; background: var(--card-base); color: var(--fg); border: 1px solid var(--border); border-radius: 8px; cursor: pointer;">
Close
</button>
</div>
</div>
</div>`);
async function loadTotpSettings() {
try {
const res = await fetch('/api/v1/totp/config');
const data = await res.json();
if (!data.success) return;
const { enabled, sessionDuration, isSetUp } = data.config;
const statusDot = document.getElementById('totp-status-dot');
const statusText = document.getElementById('totp-status-text');
const statusBanner = document.getElementById('totp-status-banner');
const setupSection = document.getElementById('totp-setup-section');
const qrSection = document.getElementById('totp-qr-section');
const durationSection = document.getElementById('totp-duration-section');
const disableSection = document.getElementById('totp-disable-section');
if (enabled && isSetUp) {
statusDot.style.background = 'var(--ok-fg, #7ef2ff)';
statusBanner.style.borderColor = 'var(--ok-fg, #7ef2ff)';
statusBanner.style.background = 'color-mix(in srgb, var(--ok-fg) 8%, transparent)';
statusText.textContent = 'TOTP is active';
statusText.style.color = 'var(--ok-fg, #7ef2ff)';
setupSection.style.display = 'none';
qrSection.style.display = 'none';
durationSection.style.display = 'block';
disableSection.style.display = 'block';
document.getElementById('totp-duration-select').value = sessionDuration;
} else {
statusDot.style.background = 'var(--muted)';
statusBanner.style.borderColor = 'var(--border)';
statusBanner.style.background = 'transparent';
statusText.textContent = 'TOTP is not configured';
statusText.style.color = 'var(--muted)';
setupSection.style.display = 'block';
qrSection.style.display = 'none';
durationSection.style.display = 'none';
disableSection.style.display = 'none';
}
// Update the Auth card in the top row
updateAuthCard(enabled && isSetUp, sessionDuration);
} catch (e) {
console.warn('Failed to load TOTP settings:', e);
}
}
// Duration label helper
const DURATION_LABELS = {
'15m': '15 min', '30m': '30 min', '1h': '1 hour', '2h': '2 hours',
'4h': '4 hours', '8h': '8 hours', '12h': '12 hours', '24h': '24 hours', 'never': 'Disabled'
};
function updateAuthCard(active, duration) {
const card = document.getElementById('auth-card');
const pill = document.getElementById('auth-pill');
const dot = document.getElementById('auth-dot');
const statusText = document.getElementById('auth-status-text');
if (!card) return;
if (active) {
card.setAttribute('data-status', 'on');
pill.className = 'badge on';
pill.textContent = 'YES';
dot.className = 'dot ok at-bl';
statusText.textContent = 'Session: ' + (DURATION_LABELS[duration] || duration);
} else {
card.setAttribute('data-status', 'off');
pill.className = 'badge off';
pill.textContent = 'NO';
dot.className = 'dot bad at-bl';
statusText.textContent = 'Not configured';
}
}
// Setup button (generate new secret)
document.getElementById('totp-setup-btn')?.addEventListener('click', async () => {
try {
const res = await secureFetch('/api/v1/totp/setup', { method: 'POST' });
const data = await res.json();
if (data.success) {
document.getElementById('totp-qr-image').src = data.qrCode;
document.getElementById('totp-manual-key').textContent = data.manualKey;
document.getElementById('totp-setup-section').style.display = 'none';
document.getElementById('totp-qr-section').style.display = 'block';
document.getElementById('totp-setup-code').value = '';
document.getElementById('totp-setup-error').textContent = '';
document.getElementById('totp-setup-code').focus();
}
} catch (e) {
console.error('TOTP setup failed:', e);
}
});
// Import existing secret button
document.getElementById('totp-import-btn')?.addEventListener('click', async () => {
const secret = document.getElementById('totp-import-key').value.trim();
if (!secret) return;
try {
const res = await secureFetch('/api/v1/totp/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ secret })
});
const data = await res.json();
if (data.success) {
document.getElementById('totp-qr-image').src = data.qrCode;
document.getElementById('totp-manual-key').textContent = data.manualKey;
document.getElementById('totp-setup-section').style.display = 'none';
document.getElementById('totp-qr-section').style.display = 'block';
document.getElementById('totp-setup-code').value = '';
document.getElementById('totp-setup-error').textContent = '';
document.getElementById('totp-setup-code').focus();
} else {
document.getElementById('totp-import-key').style.borderColor = 'var(--bad-fg)';
setTimeout(() => { document.getElementById('totp-import-key').style.borderColor = ''; }, 2000);
}
} catch (e) {
console.error('TOTP import failed:', e);
}
});
// Copy key button
document.getElementById('totp-copy-key')?.addEventListener('click', () => {
const key = document.getElementById('totp-manual-key').textContent;
navigator.clipboard.writeText(key).then(() => {
const btn = document.getElementById('totp-copy-key');
btn.textContent = '✅';
setTimeout(() => { btn.textContent = '📋'; }, 2000);
});
});
// Confirm setup
document.getElementById('totp-confirm-setup')?.addEventListener('click', async () => {
const code = document.getElementById('totp-setup-code').value;
const errorEl = document.getElementById('totp-setup-error');
if (!/^\d{6}$/.test(code)) {
errorEl.textContent = 'Enter a 6-digit code';
return;
}
try {
const res = await secureFetch('/api/v1/totp/verify-setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const data = await res.json();
if (data.success) {
errorEl.textContent = '';
loadTotpSettings(); // Updates both modal and card
} else {
errorEl.textContent = data.error || 'Invalid code';
}
} catch (e) {
errorEl.textContent = 'Connection error';
}
});
// Allow Enter key on setup code input
document.getElementById('totp-setup-code')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') document.getElementById('totp-confirm-setup')?.click();
});
// Duration change
document.getElementById('totp-duration-select')?.addEventListener('change', async (e) => {
try {
await secureFetch('/api/v1/totp/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionDuration: e.target.value })
});
loadTotpSettings(); // Refresh modal + card (handles "never" disabling TOTP)
} catch (err) {
console.error('Failed to update session duration:', err);
}
});
// Disable TOTP
document.getElementById('totp-disable-btn')?.addEventListener('click', async () => {
if (!confirm('Disable TOTP authentication? All services will be accessible without a code.')) return;
try {
const res = await secureFetch('/api/v1/totp/disable', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
});
const data = await res.json();
if (data.success) loadTotpSettings();
} catch (e) {
console.error('Failed to disable TOTP:', e);
}
});
// Open settings modal from Auth card
document.getElementById('auth-settings-btn')?.addEventListener('click', () => {
loadTotpSettings();
openModal('totp-settings-modal');
});
// Close settings modal
document.getElementById('totp-modal-close')?.addEventListener('click', () => {
closeModal('totp-settings-modal');
});
// Backdrop click to close
document.getElementById('totp-settings-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'totp-settings-modal') {
closeModal('totp-settings-modal');
}
});
// Update auth card on page load
window._updateAuthCard = updateAuthCard;
(async () => {
try {
const res = await fetch('/api/v1/totp/config');
const data = await res.json();
if (data.success) {
const active = data.config.enabled && data.config.isSetUp;
updateAuthCard(active, data.config.sessionDuration);
}
} catch (e) { console.error('[AuthCard] Failed to update:', e); }
})();
})();