Visitor sign in kiosk: multi-site, badge printing, WWCC expiry warnings, admin accounts with 2FA

This commit is contained in:
2026-08-31 14:33:30 +10:00
parent ed77493817
commit b23ad422d0
13 changed files with 843 additions and 119 deletions
+92 -2
View File
@@ -833,13 +833,21 @@ async function loadSystem() {
? `Connected. ${s.sheets.queued} row(s) waiting to send.${s.sheets.lastError ? ` Last error: ${esc(s.sheets.lastError)}` : ''}`
: 'Turned off in the environment file.'
}</dd>
<dt>Last sheet write</dt><dd>${stamp(s.sheets.lastOk)}</dd>
<dt>History tab</dt><dd>${esc(s.sheets.logTab)} — last written ${stamp(s.sheets.lastOk)}</dd>
<dt>Live tab</dt><dd>${esc(s.sheets.onSiteTab)}${
s.sheets.onSiteCount === null ? 'not synced yet' : `${s.sheets.onSiteCount} on site`
}, last synced ${stamp(s.sheets.lastOnSiteSync)}${
s.sheets.onSiteError ? ` <span class="pill bad">${esc(s.sheets.onSiteError)}</span>` : ''
}</dd>
</dl>
<div class="sys-actions">
<button class="ghost" id="sheet-test">Test the sheet connection</button>
<button class="ghost" id="sheet-flush">Send queued rows now</button>
<button class="ghost" id="sheet-resync">Rebuild the live list</button>
<button class="ghost danger" id="photo-purge">Purge photos past retention</button>
</div>`;
</div>
<h3 class="section-gap">Certificate</h3>
${renderTls(s.tls)}`;
$('#sheet-test').addEventListener('click', async () => {
try {
@@ -850,6 +858,15 @@ async function loadSystem() {
toast(err.message, true);
}
});
$('#sheet-resync').addEventListener('click', async () => {
try {
const r = await api('/sheets/sync', { method: 'POST' });
toast(r.skipped ? 'Sheet mirroring is off.' : `Live tab rewritten with ${r.rows} on site.`);
loadSystem();
} catch (err) {
toast(err.message, true);
}
});
$('#sheet-flush').addEventListener('click', async () => {
try {
const r = await api('/sheets/flush', { method: 'POST' });
@@ -859,6 +876,46 @@ async function loadSystem() {
toast(err.message, true);
}
});
$('#sheet-resync').addEventListener('click', async () => {
try {
const r = await api('/sheets/resync', { method: 'POST' });
toast(`Live list rebuilt with ${r.rows} ${r.rows === 1 ? 'person' : 'people'}.`);
loadSystem();
} catch (err) {
toast(err.message, true);
}
});
$('#renew-cert')?.addEventListener('click', async () => {
try {
const r = await api('/tls/renew', { method: 'POST', body: { newCa: false } });
toast(
r.info.server
? `Certificate good until ${new Date(r.info.server.validTo).toLocaleDateString('en-AU')}.`
: 'Certificate checked.'
);
loadSystem();
} catch (err) {
toast(err.message, true);
}
});
$('#new-ca')?.addEventListener('click', async () => {
const warning =
'Create a brand new certificate authority?' +
'\n\n' +
'Every kiosk device will show a warning until you install the new CA file on it. ' +
'Only do this if the old key may have leaked.';
if (!confirm(warning)) return;
try {
await api('/tls/renew', { method: 'POST', body: { newCa: true } });
toast('New authority created. Install it on every kiosk device.');
loadSystem();
} catch (err) {
toast(err.message, true);
}
});
$('#photo-purge').addEventListener('click', async () => {
if (!confirm('Delete photos older than the retention window? This cannot be undone.')) return;
const r = await api('/photos/purge', { method: 'POST' });
@@ -866,6 +923,39 @@ async function loadSystem() {
});
renderAccount(s);
$$('.owner-only').forEach((el) => {
el.hidden = me.role !== 'owner';
});
}
function renderTls(tls) {
if (!tls?.enabled) {
return `<p class="notice">HTTPS is off, so the kiosk camera will only work on localhost.
Set <code>HTTPS_ENABLED=true</code> in the environment file and restart.</p>`;
}
if (!tls.server) {
return '<p class="notice">HTTPS is on but no certificate could be read.</p>';
}
const soon = tls.server.daysLeft < 30;
return `
<dl>
<dt>Server certificate</dt>
<dd>Valid until ${new Date(tls.server.validTo).toLocaleDateString('en-AU')}
<span class="pill ${soon ? 'warn' : ''}">${tls.server.daysLeft} days</span></dd>
<dt>Valid for</dt><dd>${esc(tls.server.names.join(', '))}</dd>
<dt>Authority expires</dt>
<dd>${tls.ca ? new Date(tls.ca.validTo).toLocaleDateString('en-AU') : '—'}
${tls.ca ? `<span class="pill">${tls.ca.daysLeft} days</span>` : ''}</dd>
<dt>CA fingerprint</dt><dd class="fingerprint">${esc(tls.ca?.fingerprint || '—')}</dd>
</dl>
<p class="hint">Install the CA file on each kiosk device once. The server certificate renews
itself before it lapses, and devices that trust the authority keep working without being
touched again.</p>
<div class="sys-actions">
<a class="ghost" href="/admin/api/tls/ca.crt" download>Download the CA certificate</a>
<button class="ghost owner-only" id="renew-cert">Renew the server certificate</button>
<button class="ghost danger owner-only" id="new-ca">Start a new authority</button>
</div>`;
}
function renderAccount(status) {