1(function () { 2 const root = document.getElementById('o2v-portal'); 3 if (!root) return; 4 5 const loginBox = root.querySelector('.o2v-login-box'); 6 const codesBox = root.querySelector('.o2v-codes-box'); 7 const userEl = document.getElementById('o2v-username'); 8 const passEl = document.getElementById('o2v-password'); 9 const loginBtn = document.getElementById('o2v-login-btn'); 10 const logoutBtn = document.getElementById('o2v-logout-btn'); 11 const errEl = document.getElementById('o2v-login-err'); 12 const welcomeEl = document.getElementById('o2v-welcome'); 13 const listEl = document.getElementById('o2v-codes-list'); 14 const timerFill = document.getElementById('o2v-timer-fill'); 15 const timerSecs = document.getElementById('o2v-timer-secs'); 16 17 const PERIOD = 30; 18 const SESSION_LIMIT_MS = 60 * 1000; // hard logout after 1 minute, no matter what 19 let token = sessionStorage.getItem('o2v_token') || null; 20 let countdownTimer = null; // ticks every second, visual only 21 let sessionTimer = null; // fires exactly once, 60s after login 22 let remaining = PERIOD; // seconds left in the current TOTP window 23 24 function showCodes(name) { 25 loginBox.style.display = 'none'; 26 codesBox.style.display = 'block'; 27 welcomeEl.textContent = 'Signed in as ' + name; 28 } 29 30 function showLogin() { 31 codesBox.style.display = 'none'; 32 loginBox.style.display = 'block'; 33 if (countdownTimer) clearInterval(countdownTimer); 34 if (sessionTimer) clearTimeout(sessionTimer); 35 } 36 37 function escapeHtml(s) { 38 return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); 39 } 40 41 function paintTimer() { 42 timerSecs.textContent = remaining + 's'; 43 const pct = (remaining / PERIOD) * 100; 44 timerFill.style.width = pct + '%'; 45 timerFill.classList.toggle('o2v-timer-warn', remaining <= 5); 46 } 47 48 function startCountdown() { 49 if (countdownTimer) clearInterval(countdownTimer); 50 countdownTimer = setInterval(() => { 51 remaining -= 1; 52 if (remaining <= 0) { 53 fetchCodes(); // window rolled over server-side too â get the new codes now 54 return; 55 } 56 paintTimer(); 57 }, 1000); 58 } 59 60 async function doHardLogout(reason) { 61 if (!token) return; 62 const body = new URLSearchParams({ action: 'o2v_logout', nonce: O2V.nonce, token }); 63 await fetch(O2V.ajaxUrl, { method: 'POST', body }); 64 sessionStorage.removeItem('o2v_token'); 65 token = null; 66 showLogin(); 67 if (reason) errEl.textContent = reason; 68 } 69 70 // Fixed 60-second session: starts the moment login succeeds and fires 71 // regardless of mouse movement, clicks, or any other activity. 72 function startSessionTimer() { 73 if (sessionTimer) clearTimeout(sessionTimer); 74 sessionTimer = setTimeout(() => { 75 doHardLogout('Session ended after 1 minute â please log in again.'); 76 }, SESSION_LIMIT_MS); 77 } 78 79 async function fetchCodes() { 80 const body = new URLSearchParams({ action: 'o2v_get_codes', nonce: O2V.nonce, token }); 81 const res = await fetch(O2V.ajaxUrl, { method: 'POST', body }); 82 const data = await res.json(); 83 84 if (!data.success) { 85 sessionStorage.removeItem('o2v_token'); 86 token = null; 87 showLogin(); 88 errEl.textContent = data.data.message || 'Session expired.'; 89 return; 90 } 91 92 showCodes(data.data.name); 93 94 listEl.innerHTML = ''; 95 data.data.codes.forEach(acc => { 96 const card = document.createElement('div'); 97 card.className = 'o2v-account-card'; 98 card.innerHTML = 99 '<div><div class="o2v-account-label">' + escapeHtml(acc.label) + '</div>' + 100 '<div class="o2v-timer">tap to copy · refreshes automatically</div></div>' + 101 '<div class="o2v-account-code">' + escapeHtml(acc.code) + '</div>'; 102 card.addEventListener('click', () => navigator.clipboard.writeText(acc.code)); 103 listEl.appendChild(card); 104 }); 105 if (!data.data.codes.length) { 106 listEl.innerHTML = '<p style="color:#8890b5;">No accounts have been assigned to you yet.</p>'; 107 } 108 109 // Sync the visual countdown to the server's real TOTP window. 110 remaining = data.data.seconds || PERIOD; 111 paintTimer(); 112 startCountdown(); 113 } 114 115 loginBtn.addEventListener('click', async () => { 116 errEl.textContent = ''; 117 const body = new URLSearchParams({ 118 action: 'o2v_login', nonce: O2V.nonce, 119 username: userEl.value.trim(), password: passEl.value 120 }); 121 const res = await fetch(O2V.ajaxUrl, { method: 'POST', body }); 122 const data = await res.json(); 123 if (!data.success) { 124 errEl.textContent = data.data.message || 'Login failed.'; 125 return; 126 } 127 token = data.data.token; 128 sessionStorage.setItem('o2v_token', token); 129 passEl.value = ''; 130 await fetchCodes(); 131 startSessionTimer(); 132 }); 133 134 logoutBtn.addEventListener('click', () => doHardLogout()); 135 136 passEl.addEventListener('keydown', e => { if (e.key === 'Enter') loginBtn.click(); }); 137 138 // If the phone/tab was backgrounded, browsers throttle timers â 139 // resync codes when the tab becomes active again. The 60s session 140 // timer itself is untouched by activity, so it still fires on schedule. 141 document.addEventListener('visibilitychange', () => { 142 if (document.visibilityState === 'visible' && token) fetchCodes(); 143 }); 144 window.addEventListener('focus', () => { 145 if (token) fetchCodes(); 146 }); 147 148 // Note: an existing sessionStorage token from a previous page load has no 149 // way of knowing how much of its 60s already elapsed, so a fresh page 150 // load with a saved token gets a full new 60s window rather than none. 151 if (token) { 152 fetchCodes(); 153 startSessionTimer(); 154 } 155})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.