// ===== TOTP SETTINGS ===== (function() { const errorHandler = new ErrorHandler(); injectModal('totp-settings-modal', `

Authentication Settings

TOTP is not configured
or
`); 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)'; // Keep the setup section visible (collapsed) so the "Import existing // secret" option is always reachable — users may need to re-enroll // their authenticator with the same secret from a backup file. setupSection.style.display = 'block'; const setupBtn = document.getElementById('totp-setup-btn'); if (setupBtn) setupBtn.textContent = 'Generate New Secret'; // Hide the QR section by default in the active state — setupBtn click shows it 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 translated(key, fallback) { const i18n = window.DCI18n; return i18n && i18n.isLoaded() ? i18n.t(key) : fallback; } 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 = translated('card.status.yes', '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 = translated('card.status.no', 'NO'); dot.className = 'dot bad at-bl'; statusText.textContent = translated('card.auth.not_configured', '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) { errorHandler.logError('[TOTP] Setup Failed', e, { function: 'setupTOTP' }); } }); // Import existing secret button document.getElementById('totp-import-btn')?.addEventListener('click', async () => { const secret = document.getElementById('totp-import-key').value.trim(); const errorEl = document.getElementById('totp-import-error'); errorEl.textContent = ''; if (!secret) { errorEl.textContent = 'Paste a Base32 secret key first'; 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) { errorEl.textContent = ''; 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 { errorEl.textContent = data.error || data.message || 'Import failed'; } } catch (e) { errorEl.textContent = 'Connection error — try refreshing the page'; } }); // 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); }); }); // Download backup file — plain JSON so it round-trips through any password // manager, cloud backup, or printed paper. The secret IS recoverable plaintext // (that's the whole point of the backup), so warn the user and rely on // them to keep it safe. document.getElementById('totp-download-backup')?.addEventListener('click', () => { const secret = document.getElementById('totp-manual-key').textContent.trim(); if (!secret) return; const payload = { service: 'DashCaddy', type: 'totp-secret', secret: secret, issuer: 'DashCaddy', algorithm: 'SHA1', digits: 6, period: 30, issued: new Date().toISOString(), // Recovery instructions baked into the file so a year from now the // user (or their future self) knows what this file is and how to use it. recovery_url: `${window.location.origin}/ (login screen → "Lost access?")`, note: 'Keep this file somewhere safe. Anyone with this secret can generate your login codes. Use it ONLY to recover TOTP access via the "Lost access?" link on the DashCaddy login screen.' }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `dashcaddy-totp-backup-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); const btn = document.getElementById('totp-download-backup'); btn.textContent = '✅ Saved'; setTimeout(() => { btn.textContent = '⬇ Download'; }, 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) { errorHandler.logError('[TOTP] Update Session Duration', err, { function: 'updateSessionDuration' }); } }); // 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) { errorHandler.logError('[TOTP] Disable Failed', e, { function: 'disableTOTP' }); } }); // 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) { errorHandler.logError('[TOTP] AuthCard Update', e, { function: 'authCardUpdate' }); } })(); })();