// The PIN lock: in a new browser tab the panel asks for the admin's PIN // before showing anything. The tab remembers it was unlocked (sessionStorage // lives as long as the tab, through reloads, and a new tab starts empty), so // a reload does not ask again. Loaded in , before the page shows. // A device can be set to trust the PIN for a while: for that many minutes // after it was entered, new tabs there open without asking (localStorage, // this browser only). (function () { 'use strict'; var KEY = 'lpra.pin.ok'; var ok = false; try { ok = sessionStorage.getItem(KEY) === '1'; } catch (e) { /* no storage: ask */ } if (ok) return; try { if (Date.now() < Number(localStorage.getItem('lpra.pin.until') || 0)) { sessionStorage.setItem(KEY, '1'); return; } } catch (e) { /* ask */ } var root = document.documentElement; root.classList.add('pin-wait'); function release() { root.classList.remove('pin-wait'); } function remember() { try { sessionStorage.setItem(KEY, '1'); } catch (e) { /* asked again next load */ } } // After the PIN itself: no asking on this device for the minutes it trusts. function trust() { try { var min = Number(localStorage.getItem('lpra.pin.grace') || 0); if (min > 0) localStorage.setItem('lpra.pin.until', String(Date.now() + min * 60000)); } catch (e) { /* asks every tab */ } } fetch('/api/panel/pin', {cache: 'no-store'}) .then(function (r) { return r.ok ? r.json() : {pin: false}; }) .then(function (d) { if (!d.pin) { remember(); release(); return; } if (document.body) lock(); else document.addEventListener('DOMContentLoaded', lock); }) .catch(release); function lock() { var LOCK = ''; var BACK = ''; var box = document.createElement('div'); box.className = 'pinlock'; box.setAttribute('role', 'dialog'); box.setAttribute('aria-modal', 'true'); box.setAttribute('aria-label', 'Ввод PIN-кода'); var keys = ''; ['1', '2', '3', '4', '5', '6', '7', '8', '9', '', '0', 'back'].forEach(function (k) { if (!k) { keys += ''; return; } keys += ''; }); box.innerHTML = '
' + '
' + LOCK + '
' + '
Введите PIN-код
' + '
' + '
' + '
' + keys + '
' + '
' + '
'; document.body.appendChild(box); requestAnimationFrame(function () { box.classList.add('on'); }); var dots = box.querySelectorAll('.pin-dots i'), msg = box.querySelector('.pin-msg'), card = box.querySelector('.pin-card'); var code = '', busy = false; function show() { for (var i = 0; i < 4; i++) dots[i].classList.toggle('on', i < code.length); } function press(k) { if (busy) return; if (k === 'back') { code = code.slice(0, -1); show(); return; } if (code.length >= 4) return; code += k; msg.textContent = ''; card.classList.remove('bad'); show(); if (code.length === 4) check(); } function check() { busy = true; fetch('/api/panel/pin/check', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({pin: code})}) .then(function (r) { return r.json(); }) .then(function (r) { if (r.ok) { remember(); trust(); box.classList.add('done'); release(); setTimeout(function () { box.remove(); }, 320); document.removeEventListener('keydown', onKey, true); return; } if (r.out) { location.href = '/login'; return; } card.classList.remove('bad'); void card.offsetWidth; // restart the shake card.classList.add('bad'); msg.textContent = r.error + (r.left ? ' - осталось попыток: ' + r.left : ''); setTimeout(function () { code = ''; show(); busy = false; }, 380); }) .catch(function () { msg.textContent = 'нет связи с сервером'; code = ''; show(); busy = false; }); } box.querySelector('.pin-pad').addEventListener('click', function (ev) { var b = ev.target.closest('button'); if (b) press(b.dataset.k); }); // Typed digits, ahead of anything on the page (the terminal included). function onKey(ev) { if (/^[0-9]$/.test(ev.key)) press(ev.key); else if (ev.key === 'Backspace') press('back'); else if (ev.key !== 'Tab') return; ev.preventDefault(); ev.stopPropagation(); } document.addEventListener('keydown', onKey, true); if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); } })();