1
0
mirror of git://f0xx.org/ac/ac-be-auth synced 2026-07-29 09:38:25 +03:00

feat(2fa): cross-device approval UI, polling JS, approve view

- two_factor_challenge.php: QR now shown above the code field with
  data-poll-token/url attrs; approval status banner; updated caption
- two_factor_approve.php: new mobile confirmation page (Approve/Cancel)
- two_factor.js: adds poll loop every 2.5s; auto-redirect on approval;
  expires QR visually; stops polling on manual TOTP submit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-06-27 22:49:50 +02:00
parent 393c0c1f29
commit d8d15d77ad
3 changed files with 183 additions and 10 deletions

View File

@@ -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') {