feat: a device can skip the PIN for 5, 15, 30 or 60 minutes after it was entered (off by default, this browser only)
This commit is contained in:
1 parent
119b524825
commit
5a204ae9bb
3 files changed
+36
-1
No files matched your search
@@ -54,7 +54,11 @@
|
|||||||
' <input type="password" name="pin" inputmode="numeric" maxlength="4" pattern="[0-9]{4}" placeholder="новый PIN, 4 цифры" autocomplete="off">' +
|
' <input type="password" name="pin" inputmode="numeric" maxlength="4" pattern="[0-9]{4}" placeholder="новый PIN, 4 цифры" autocomplete="off">' +
|
||||||
' <input type="password" name="again" inputmode="numeric" maxlength="4" pattern="[0-9]{4}" placeholder="новый ещё раз" autocomplete="off">' +
|
' <input type="password" name="again" inputmode="numeric" maxlength="4" pattern="[0-9]{4}" placeholder="новый ещё раз" autocomplete="off">' +
|
||||||
' <div class="row"><button type="button" class="btn danger small acct-pindel">Убрать PIN</button><button type="submit" class="acct-pinsave">Сохранить</button></div>' +
|
' <div class="row"><button type="button" class="btn danger small acct-pindel">Убрать PIN</button><button type="submit" class="acct-pinsave">Сохранить</button></div>' +
|
||||||
' </form><p class="acct-pinmsg" aria-live="polite"></p><p class="muted" style="font-size:12px;margin:8px 0 0">Спрашивается в каждой новой вкладке; при перезагрузке той же вкладки - нет.</p></details>' +
|
' </form>' +
|
||||||
|
' <label class="acct-pingrace"><span>На этом устройстве не спрашивать после ввода</span><select>' +
|
||||||
|
' <option value="0">каждый раз</option><option value="5">5 минут</option><option value="15">15 минут</option>' +
|
||||||
|
' <option value="30">30 минут</option><option value="60">60 минут</option></select></label>' +
|
||||||
|
' <p class="acct-pinmsg" aria-live="polite"></p><p class="muted" style="font-size:12px;margin:8px 0 0">Спрашивается в каждой новой вкладке; при перезагрузке той же вкладки - нет.</p></details>' +
|
||||||
' <form method="post" action="/logout" class="acct-out"><input type="hidden" name="csrf"><button type="submit" class="btn ghost">Выйти</button></form>' +
|
' <form method="post" action="/logout" class="acct-out"><input type="hidden" name="csrf"><button type="submit" class="btn ghost">Выйти</button></form>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
document.body.appendChild(dlg);
|
document.body.appendChild(dlg);
|
||||||
@@ -76,6 +80,8 @@
|
|||||||
pf.current.hidden = !d.pin;
|
pf.current.hidden = !d.pin;
|
||||||
pf.querySelector('.acct-pindel').hidden = !d.pin;
|
pf.querySelector('.acct-pindel').hidden = !d.pin;
|
||||||
pf.querySelector('.acct-pinsave').textContent = 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-ava').textContent = (d.username || '?').slice(0, 1).toUpperCase();
|
||||||
$('.acct-mode').textContent = d.password_off ? 'вход только по ключу доступа' : d.keys.length ? 'вход по паролю или ключу' : 'вход по паролю';
|
$('.acct-mode').textContent = d.password_off ? 'вход только по ключу доступа' : d.keys.length ? 'вход по паролю или ключу' : 'вход по паролю';
|
||||||
const sw = $('.acct-pw .sw');
|
const sw = $('.acct-pw .sw');
|
||||||
@@ -172,6 +178,15 @@
|
|||||||
if (f.pin.value !== f.again.value) { pinMsg('новые PIN не совпадают', true); return; }
|
if (f.pin.value !== f.again.value) { pinMsg('новые PIN не совпадают', true); return; }
|
||||||
savePin(f.pin.value);
|
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 () {
|
$('.acct-pindel').onclick = function () {
|
||||||
if (confirm('Убрать PIN-код?')) savePin('');
|
if (confirm('Убрать PIN-код?')) savePin('');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -189,6 +189,9 @@ input.sw:disabled { opacity: .45; cursor: not-allowed; }
|
|||||||
.acct-pinform { display: grid; gap: 8px; margin-top: 10px; }
|
.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 input:not(:placeholder-shown) { letter-spacing: .4em; font-family: ui-monospace, Menlo, monospace; }
|
||||||
.acct-pinform .row { justify-content: flex-end; }
|
.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 { margin: 8px 0 0; font-size: 13px; color: var(--ok, #3fb950); min-height: 0; }
|
||||||
.acct-pinmsg.bad { color: var(--danger); }
|
.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; }
|
.acct-out { margin-top: 10px; padding-top: 14px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; }
|
||||||
|
|||||||
@@ -2,16 +2,32 @@
|
|||||||
// before showing anything. The tab remembers it was unlocked (sessionStorage
|
// 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
|
// lives as long as the tab, through reloads, and a new tab starts empty), so
|
||||||
// a reload does not ask again. Loaded in <head>, before the page shows.
|
// a reload does not ask again. Loaded in <head>, 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 () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
var KEY = 'lpra.pin.ok';
|
var KEY = 'lpra.pin.ok';
|
||||||
var ok = false;
|
var ok = false;
|
||||||
try { ok = sessionStorage.getItem(KEY) === '1'; } catch (e) { /* no storage: ask */ }
|
try { ok = sessionStorage.getItem(KEY) === '1'; } catch (e) { /* no storage: ask */ }
|
||||||
if (ok) return;
|
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;
|
var root = document.documentElement;
|
||||||
root.classList.add('pin-wait');
|
root.classList.add('pin-wait');
|
||||||
function release() { root.classList.remove('pin-wait'); }
|
function release() { root.classList.remove('pin-wait'); }
|
||||||
function remember() { try { sessionStorage.setItem(KEY, '1'); } catch (e) { /* asked again next load */ } }
|
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'})
|
fetch('/api/panel/pin', {cache: 'no-store'})
|
||||||
.then(function (r) { return r.ok ? r.json() : {pin: false}; })
|
.then(function (r) { return r.ok ? r.json() : {pin: false}; })
|
||||||
@@ -67,6 +83,7 @@
|
|||||||
.then(function (r) {
|
.then(function (r) {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
remember();
|
remember();
|
||||||
|
trust();
|
||||||
box.classList.add('done');
|
box.classList.add('done');
|
||||||
release();
|
release();
|
||||||
setTimeout(function () { box.remove(); }, 320);
|
setTimeout(function () { box.remove(); }, 320);
|
||||||
|
|||||||
Reference in new issue
Block a user