diff --git a/public/assets/js/two_factor.js b/public/assets/js/two_factor.js index 3e5ec56..408a26a 100644 --- a/public/assets/js/two_factor.js +++ b/public/assets/js/two_factor.js @@ -2,14 +2,16 @@ 'use strict'; function boot() { - var form = document.querySelector('form.login-card'); - var input = document.getElementById('twofa-code-input'); + var form = document.querySelector('form.login-card'); + var input = document.getElementById('twofa-code-input'); + var script = document.currentScript || document.querySelector('script[data-poll-token]'); if (!form || !input) { return; } input.focus(); input.select(); + // ── 6-digit TOTP input helpers ───────────────────────────────────────── function digitsOnly(val) { return String(val || '').replace(/\D/g, '').slice(0, 6); } @@ -56,6 +58,91 @@ input.value = digitsOnly(input.value + ev.key); maybeSubmit(); }); + + // ── Cross-device approval polling ────────────────────────────────────── + var pollToken = script && script.getAttribute('data-poll-token'); + var pollUrl = script && script.getAttribute('data-poll-url'); + if (!pollToken || !pollUrl) { + return; + } + + var statusEl = document.getElementById('twofa-approval-status'); + var qrWrap = document.getElementById('twofa-qr-wrap'); + var pollTimer = null; + var pollActive = true; + var INTERVAL = 2500; // ms + + function showStatus(msg) { + if (statusEl) { + statusEl.style.display = ''; + statusEl.textContent = msg; + } + } + + function stopPolling() { + pollActive = false; + if (pollTimer) { + clearTimeout(pollTimer); + pollTimer = null; + } + } + + function poll() { + if (!pollActive) { + return; + } + var url = pollUrl + '?token=' + encodeURIComponent(pollToken); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onload = function () { + if (!pollActive) { + return; + } + try { + var data = JSON.parse(xhr.responseText); + } catch (e) { + scheduleNext(); + return; + } + if (data.status === 'approved' && data.redirect) { + stopPolling(); + showStatus('✅ Approved — signing you in…'); + setTimeout(function () { + window.location.href = data.redirect; + }, 400); + return; + } + if (data.status === 'expired' || data.status === 'used' || data.status === 'not_found') { + stopPolling(); + showStatus('⚠️ QR code expired. Reload to get a new one.'); + if (qrWrap) { + qrWrap.style.opacity = '0.35'; + } + return; + } + // status === 'pending' — keep polling + showStatus('⏳ Waiting for mobile approval…'); + scheduleNext(); + }; + xhr.onerror = function () { + scheduleNext(); + }; + xhr.send(); + } + + function scheduleNext() { + if (pollActive) { + pollTimer = setTimeout(poll, INTERVAL); + } + } + + // Start polling after a short delay so the page renders first + pollTimer = setTimeout(poll, 1000); + + // Stop polling if the user submits the TOTP form manually + form.addEventListener('submit', function () { + stopPolling(); + }); } if (document.readyState === 'loading') { diff --git a/views/two_factor_approve.php b/views/two_factor_approve.php new file mode 100644 index 0000000..2471c9d --- /dev/null +++ b/views/two_factor_approve.php @@ -0,0 +1,71 @@ + + + +
+ + ++ Sign-in approved. +
+Your other device should now be logged in automatically.
+ +This approval link has expired or is invalid.
+ ++ Back to hub +
+ + + +Approve sign-in on another device?
+
+ Tap Approve to log in the browser that showed you this QR code.
+ This link expires in a few minutes.
+
+ Cancel +
+ + + +This approval link has expired or has already been used.
++ Back to hub +
+ + +