Audit log (admin Activity list) and passkey recovery codes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8399ccaa7
commit
561b6181a0
8 files changed
+101
-14
No files matched your search
@@ -1659,6 +1659,8 @@ function viewSettings() {
|
||||
<div class="bar"><a class="btn ghost" href="/api/backup" download>Download a backup</a></div><p class="sub">A .tar.gz of everything in /data (accounts, passkeys, tasks, events, settings, Signal history). Keep it private: it holds the session secret and encrypted sign-in tokens. Restore by unpacking it into the data volume.</p>` : ''}
|
||||
<h2>Passkeys</h2><div class="list" id="pks"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>
|
||||
<div class="bar"><button class="btn ghost" id="addpk">Add a passkey</button><button class="btn ghost" id="invpk">Invite a device</button><button class="btn danger" id="hubout">Sign out of NE Hub</button></div>
|
||||
<h2>Recovery codes</h2><div class="list"><div class="row"><span class="grow"><b id="rcn">Checking…</b><small style="white-space:normal">One-time codes that let you add a new passkey if you lose every device. Keep them somewhere safe (a password manager or on paper).</small></span><button class="btn ghost" id="rcgen">Make codes</button></div></div>
|
||||
${isAdmin ? `<h2>Activity</h2><div class="list" id="audit"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>` : ''}
|
||||
<div id="sec-conn"><h2>Connections</h2><div class="list">
|
||||
<div class="row" id="row-sf"><span class="grow"><b>Seafile</b><small id="sfst">${esc(cfg.seafile)} · ${signed ? 'Checking…' : 'signed out'}</small></span></div>
|
||||
<div class="row" id="row-plex"><span class="grow"><b>Plex</b><small id="plexst">Checking…</small></span><button class="btn danger" id="plexout" hidden>Sign out</button></div>
|
||||
@@ -1740,6 +1742,20 @@ function viewSettings() {
|
||||
$('#addpk').onclick = () => textSheet('Add a passkey', 'Name this device', 'Create', async name => {
|
||||
try { await passkeyRegister('', name); toast('Passkey added'); loadPks(); } catch (e) { toast(e.name === 'NotAllowedError' ? 'Cancelled' : e.message); }
|
||||
});
|
||||
jfetch('/api/auth/recovery').then(r => { $('#rcn').textContent = r.remaining ? r.remaining + ' recovery code' + (r.remaining === 1 ? '' : 's') + ' left' : 'No recovery codes yet'; }).catch(() => { $('#rcn').textContent = 'Recovery codes'; });
|
||||
$('#rcgen').onclick = () => confirmSheet('Make new recovery codes?', 'Any codes you made before stop working.', 'Make codes', async () => {
|
||||
try {
|
||||
const { codes } = await jfetch('/api/auth/recovery', { method: 'POST', body: '{}' });
|
||||
const el = openSheet('Your recovery codes', 'Each works once. They are shown only now: save them before closing.', `<div class="list" style="background:var(--panel2);border:0">${codes.map(c => `<div class="row"><span class="grow"><b style="font-family:ui-monospace,Menlo,monospace;letter-spacing:.08em">${esc(c)}</b></span></div>`).join('')}</div>
|
||||
<div class="bar"><button class="btn ghost" id="rcc" style="flex:1;justify-content:center">Copy</button><button class="btn" id="sx" style="flex:1;justify-content:center">Done</button></div>`);
|
||||
el.querySelector('#sx').onclick = () => { closeSheet(); viewSettings(); };
|
||||
el.querySelector('#rcc').onclick = async () => { try { await navigator.clipboard.writeText(codes.join('\n')); toast('Copied'); } catch { toast('Could not copy; write them down'); } };
|
||||
} catch (e) { toast(e.message); }
|
||||
});
|
||||
if ($('#audit')) jfetch('/api/audit').then(({ events }) => {
|
||||
const el = $('#audit'); if (!el) return;
|
||||
el.innerHTML = events.length ? events.slice(0, 40).map(e => `<div class="row"><span class="grow"><b>${esc(e.event)}${e.who ? ' · ' + esc(e.who) : ''}</b><small style="white-space:normal">${esc(new Date(e.t).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }))}${e.detail ? ' · ' + esc(e.detail) : ''}${e.ip ? ' · ' + esc(e.ip) : ''}</small></span></div>`).join('') : '<div class="row"><span class="grow"><small>Nothing yet</small></span></div>';
|
||||
}).catch(() => { const el = $('#audit'); if (el) el.innerHTML = '<div class="row"><span class="grow"><small>Could not load</small></span></div>'; });
|
||||
$('#invpk').onclick = async () => {
|
||||
try {
|
||||
const { code, minutes } = await authPost('/api/auth/invite');
|
||||
|
||||
+3
-3
@@ -14,11 +14,11 @@ async function init() {
|
||||
$('#sub').textContent = 'Sign in with your passkey';
|
||||
$('#ui').innerHTML = '<button class="btn" id="go">Sign in with passkey</button>';
|
||||
$('#go').onclick = signIn;
|
||||
$('#ui').insertAdjacentHTML('beforeend', '<p style="margin-top:18px"><a href="#" id="inv" style="color:var(--accent)">New device? Use an invite code</a></p>');
|
||||
$('#ui').insertAdjacentHTML('beforeend', '<p style="margin-top:18px"><a href="#" id="inv" style="color:var(--accent)">New device or lost your devices? Use an invite or recovery code</a></p>');
|
||||
$('#inv').onclick = e => {
|
||||
e.preventDefault(); $('#err').textContent = ''; $('#sub').textContent = 'Add this device';
|
||||
$('#ui').innerHTML = `<form id="f"><p class="sub">On a device that is already signed in, open Settings and tap <b>Invite a device</b>.</p>
|
||||
<label for="ic">Invite code</label><input type="text" id="ic" placeholder="ABCD-2345" autocomplete="off" autocapitalize="characters" required>
|
||||
$('#ui').innerHTML = `<form id="f"><p class="sub">On a device that is already signed in, open Settings and tap <b>Invite a device</b>. Lost every device? Use one of the <b>recovery codes</b> you made in Settings.</p>
|
||||
<label for="ic">Invite or recovery code</label><input type="text" id="ic" placeholder="ABCD-2345" autocomplete="off" autocapitalize="characters" required>
|
||||
<label for="nm">Name this passkey</label><input type="text" id="nm" value="${/iPad|iPhone/.test(navigator.userAgent) ? 'iPhone/iPad' : 'This device'}" autocomplete="off">
|
||||
<div class="bar"><button class="btn">Create passkey</button></div><p style="text-align:center"><a href="/login" style="color:var(--accent)">Back to sign in</a></p></form>`;
|
||||
$('#f').onsubmit = async ev => {
|
||||
|
||||
Reference in new issue
Block a user