diff --git a/internal/server/web/static/account.js b/internal/server/web/static/account.js index 6995a56..056741b 100644 --- a/internal/server/web/static/account.js +++ b/internal/server/web/static/account.js @@ -54,7 +54,11 @@ ' ' + ' ' + '
' + - 'Спрашивается в каждой новой вкладке; при перезагрузке той же вкладки - нет.
' + + ' ' + + ' ' + + 'Спрашивается в каждой новой вкладке; при перезагрузке той же вкладки - нет.
' + ' ' + ''; document.body.appendChild(dlg); @@ -76,6 +80,8 @@ pf.current.hidden = !d.pin; pf.querySelector('.acct-pindel').hidden = !d.pin; pf.querySelector('.acct-pinsave').textContent = d.pin ? 'Сменить' : 'Установить'; + $('.acct-pingrace').hidden = !d.pin; + try { $('.acct-pingrace select').value = localStorage.getItem('lpra.pin.grace') || '0'; } catch (e) { /* default */ } $('.acct-ava').textContent = (d.username || '?').slice(0, 1).toUpperCase(); $('.acct-mode').textContent = d.password_off ? 'вход только по ключу доступа' : d.keys.length ? 'вход по паролю или ключу' : 'вход по паролю'; const sw = $('.acct-pw .sw'); @@ -172,6 +178,15 @@ if (f.pin.value !== f.again.value) { pinMsg('новые PIN не совпадают', true); return; } savePin(f.pin.value); }; + $('.acct-pingrace select').onchange = function (ev) { + const min = Number(ev.target.value); + try { + localStorage.setItem('lpra.pin.grace', String(min)); + // Counted from now, as the PIN was entered to get here. + localStorage.setItem('lpra.pin.until', min ? String(Date.now() + min * 60000) : '0'); + } catch (e) { pinMsg('браузер не даёт сохранить настройку', true); return; } + pinMsg(min ? 'на этом устройстве PIN не спросят ' + min + ' мин после ввода' : 'PIN спрашивается в каждой новой вкладке'); + }; $('.acct-pindel').onclick = function () { if (confirm('Убрать PIN-код?')) savePin(''); }; diff --git a/internal/server/web/static/app.css b/internal/server/web/static/app.css index 1e38921..a4d74f1 100644 --- a/internal/server/web/static/app.css +++ b/internal/server/web/static/app.css @@ -189,6 +189,9 @@ input.sw:disabled { opacity: .45; cursor: not-allowed; } .acct-pinform { display: grid; gap: 8px; margin-top: 10px; } .acct-pinform input:not(:placeholder-shown) { letter-spacing: .4em; font-family: ui-monospace, Menlo, monospace; } .acct-pinform .row { justify-content: flex-end; } +.acct-pingrace { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 12px; font-size: 13px; color: var(--muted); } +.acct-pingrace[hidden] { display: none; } +.acct-pingrace select { width: auto; flex: none; } .acct-pinmsg { margin: 8px 0 0; font-size: 13px; color: var(--ok, #3fb950); min-height: 0; } .acct-pinmsg.bad { color: var(--danger); } .acct-out { margin-top: 10px; padding-top: 14px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; } diff --git a/internal/server/web/static/pin.js b/internal/server/web/static/pin.js index cc922d9..12cb0f1 100644 --- a/internal/server/web/static/pin.js +++ b/internal/server/web/static/pin.js @@ -2,16 +2,32 @@ // 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}; }) @@ -67,6 +83,7 @@ .then(function (r) { if (r.ok) { remember(); + trust(); box.classList.add('done'); release(); setTimeout(function () { box.remove(); }, 320);