// ===== TOTP AUTHENTICATION GATE ===== (function() { function updateTotpLogo() { const card = document.querySelector('.totp-card'); if (!card) return; const bg = getComputedStyle(card).backgroundColor; const m = bg.match(/\d+/g); if (!m) return; const lum = (0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2]) / 255; const dark = card.querySelector('.totp-logo-dark'); const light = card.querySelector('.totp-logo-light'); if (dark) dark.style.display = lum > 0.5 ? 'none' : ''; if (light) light.style.display = lum > 0.5 ? '' : 'none'; } function showTotpOverlay() { const overlay = document.getElementById('totp-overlay'); if (overlay) { overlay.classList.add('show'); setTimeout(updateTotpLogo, 50); const firstInput = overlay.querySelector('.totp-digits input'); if (firstInput) setTimeout(() => firstInput.focus(), 100); } } function hideTotpOverlay() { const overlay = document.getElementById('totp-overlay'); if (overlay) overlay.classList.remove('show'); } // Setup digit input UX const container = document.getElementById('totp-digits'); if (container) { const inputs = container.querySelectorAll('input'); inputs.forEach((input, idx) => { input.addEventListener('input', (e) => { const val = e.target.value.replace(/\D/g, ''); e.target.value = val.slice(0, 1); if (val && idx < inputs.length - 1) inputs[idx + 1].focus(); const code = Array.from(inputs).map(i => i.value).join(''); if (code.length === 6) submitTotpCode(code); }); input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !e.target.value && idx > 0) { inputs[idx - 1].focus(); inputs[idx - 1].value = ''; } }); input.addEventListener('paste', (e) => { e.preventDefault(); const pasted = (e.clipboardData.getData('text') || '').replace(/\D/g, ''); if (pasted.length >= 6) { inputs.forEach((inp, i) => { inp.value = pasted[i] || ''; }); inputs[5].focus(); submitTotpCode(pasted.slice(0, 6)); } }); }); } async function submitTotpCode(code) { const errorEl = document.getElementById('totp-error'); errorEl.textContent = 'Verifying...'; errorEl.className = 'totp-error verifying'; try { const res = await secureFetch('/api/v1/totp/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) }); const data = await res.json(); if (data.success) { errorEl.textContent = ''; hideTotpOverlay(); // Check if redirected here from another service const redirect = safeSessionGet('totp_redirect'); if (redirect) { try { sessionStorage.removeItem('totp_redirect'); } catch (_) {} window.location.href = redirect; return; } // Initialize dashboard if (typeof window.initializeDashboard === 'function') { window.initializeDashboard(); } } else { errorEl.textContent = data.error || 'Invalid code'; errorEl.className = 'totp-error'; const inputs = document.querySelectorAll('#totp-digits input'); inputs.forEach(i => { i.value = ''; }); inputs[0]?.focus(); } } catch (e) { errorEl.textContent = 'Connection error'; errorEl.className = 'totp-error'; } } // Handle ?auth=required redirect from Caddy SSO const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('auth') === 'required') { const returnUrl = urlParams.get('return'); if (returnUrl) { // Validate redirect URL: must be same-origin or hostname must end with our TLD // (prevents open redirect via includes() bypass like evil.com?q=.sami) try { const parsed = new URL(returnUrl, window.location.origin); const hostname = parsed.hostname; const isSameOrigin = parsed.origin === window.location.origin; const tldSuffix = SITE.tld.startsWith('.') ? SITE.tld : '.' + SITE.tld; const isOurTld = hostname.endsWith(tldSuffix) || hostname === tldSuffix.substring(1); if (isSameOrigin || isOurTld) { safeSessionSet('totp_redirect', returnUrl); } } catch (_) { // Invalid URL — reject redirect } } // Clean URL window.history.replaceState({}, '', window.location.pathname); } window._showTotpOverlay = showTotpOverlay; })();