// The account button in the top bar: a dialog with who is signed in, their // passkeys (add one, remove one), the password switch and signing out. The // password can be turned off once there is a passkey to sign in with. (function () { 'use strict'; var btn = document.getElementById('acct'); if (!btn) return; var b64u = { enc: function (buf) { var s = '', b = new Uint8Array(buf); for (var i = 0; i < b.length; i++) s += String.fromCharCode(b[i]); return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }, dec: function (str) { var s = atob(str.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((str.length + 3) % 4)); var b = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) b[i] = s.charCodeAt(i); return b.buffer; } }; var esc = function (s) { return String(s).replace(/[&<>"]/g, function (c) { return {'&': '&', '<': '<', '>': '>', '"': '"'}[c]; }); }; var when = function (t) { return t ? new Date(t * 1000).toLocaleString() : 'ещё не использовался'; }; var csrf = (document.cookie.match(/(?:^|; )lpra_csrf=([^;]*)/) || [])[1] || ''; var X = ''; var KEY = ''; var dlg = document.createElement('dialog'); dlg.id = 'acctdlg'; dlg.tabIndex = -1; dlg.innerHTML = '
Аккаунт' + '
' + '
' + '
' + '

Ключи доступа

' + '

Вход отпечатком, лицом, PIN-кодом устройства или аппаратным ключом. Ключей может быть несколько - например, телефон и ноутбук.

' + ' ' + '
' + '
' + '

' + ' ' + '
Сменить пароль' + '
' + ' ' + ' ' + ' ' + ' ' + '
' + '
' + '
'; document.body.appendChild(dlg); dlg.querySelector('.acct-out [name=csrf]').value = decodeURIComponent(csrf); var $ = function (sel) { return dlg.querySelector(sel); }; $('[data-close]').onclick = function () { dlg.close(); }; var supported = !!(window.PublicKeyCredential && navigator.credentials && window.isSecureContext); function msg(text, bad) { $('.acct-msg').textContent = text || ''; $('.acct-msg').className = 'acct-msg' + (bad ? ' bad' : ''); } function load() { return fetch('/api/panel/account', {cache: 'no-store'}).then(function (r) { return r.json(); }).then(function (d) { $('.acct-name').textContent = d.username; dlg.querySelector('.acct-pwform [name=username]').value = d.username; $('.acct-ava').textContent = (d.username || '?').slice(0, 1).toUpperCase(); $('.acct-mode').textContent = d.password_off ? 'вход только по ключу доступа' : d.keys.length ? 'вход по паролю или ключу' : 'вход по паролю'; const sw = $('.acct-pw .sw'); sw.checked = !d.password_off; sw.disabled = !d.keys.length; $('.acct-pwnote').textContent = !d.keys.length ? 'выключить можно, когда есть хотя бы один ключ' : d.password_off ? 'выключен: войти можно только ключом' : 'можно выключить и входить только ключом'; var ul = $('.acct-keys'); ul.innerHTML = d.keys.length ? '' : '
  • Ключей пока нет.
  • '; d.keys.forEach(function (k) { var li = document.createElement('li'); li.innerHTML = '' + KEY + '
    ' + esc(k.name) + '
    ' + '
    добавлен ' + esc(when(k.created)) + ' · вход: ' + esc(when(k.last_used)) + '
    ' + ''; li.querySelector('button').onclick = function () { var last = d.keys.length === 1; if (!confirm('Удалить ключ «' + k.name + '»?' + (last && d.password_off ? ' Это последний ключ: вход по паролю снова включится.' : ''))) return; fetch('/api/panel/account/keys/' + k.id + '/delete', {method: 'POST'}).then(function (r) { return r.json(); }) .then(function (r) { if (!r.ok) msg(r.error, true); else { msg('ключ удалён'); load(); } }); }; ul.appendChild(li); }); }); } $('.acct-addbtn').onclick = function () { if (!supported) { msg('этот браузер не умеет ключи доступа, или страница открыта не по https', true); return; } var add = $('.acct-addbtn'); add.disabled = true; msg('подтвердите на устройстве…'); fetch('/api/panel/account/keys/begin', {method: 'POST'}).then(function (r) { return r.json(); }).then(function (r) { var o = r.options; o.challenge = b64u.dec(o.challenge); o.user.id = b64u.dec(o.user.id); o.excludeCredentials.forEach(function (c) { c.id = b64u.dec(c.id); }); return navigator.credentials.create({publicKey: o}); }).then(function (cred) { return fetch('/api/panel/account/keys/finish', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name: $('.acct-keyname').value, clientDataJSON: b64u.enc(cred.response.clientDataJSON), attestationObject: b64u.enc(cred.response.attestationObject) }) }).then(function (r) { return r.json(); }); }).then(function (r) { if (!r.ok) { msg(r.error, true); return; } $('.acct-keyname').value = ''; msg('ключ добавлен - теперь вход по нему'); load(); }).catch(function (e) { msg(e && e.name === 'NotAllowedError' ? 'отменено' : e && e.name === 'InvalidStateError' ? 'этот ключ уже добавлен' : 'не вышло: ' + (e && e.message || e), true); }).then(function () { add.disabled = false; }); }; $('.acct-pwform').onsubmit = function (ev) { ev.preventDefault(); const f = ev.target; if (f.new.value !== f.again.value) { msg('новые пароли не совпадают', true); return; } f.querySelector('button').disabled = true; fetch('/api/panel/account/password/change', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({current: f.current.value, new: f.new.value})}) .then(function (r) { return r.json(); }) .then(function (r) { if (!r.ok) { msg(r.error, true); return; } f.reset(); dlg.querySelector('.acct-chpw').open = false; msg('пароль сменён'); }) .catch(function () { msg('ошибка сети', true); }) .then(function () { f.querySelector('button').disabled = false; }); }; $('.acct-pw .sw').onchange = function (ev) { const off = !ev.target.checked; if (off && !confirm('Выключить вход по паролю? Войти можно будет только ключом доступа. Если ключи потеряются, их сможет сбросить другой администратор в «Настройках».')) { ev.target.checked = true; return; } fetch('/api/panel/account/password', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({off: off})}) .then(function (r) { return r.json(); }) .then(function (r) { if (!r.ok) msg(r.error, true); else msg(off ? 'вход по паролю выключен' : 'вход по паролю включён'); load(); }); }; btn.onclick = function () { msg(''); load().then(function () { dlg.showModal(); dlg.focus(); }); }; })();