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

This commit is contained in:
2026-09-02 16:07:52 +10:00
parent c21123885d
commit 74792b2764
11 changed files with 469 additions and 11 deletions
+59 -2
View File
@@ -10,6 +10,13 @@ import * as sheets from '../sheets.js';
import * as tls from '../tls.js';
import * as users from '../users.js';
import { badgeHtml, listSites, shapeSite, uniqueSlug, escapeHtml as esc } from '../sites.js';
import {
DEFAULT_THEME,
bannerAbsolutePath,
deleteBanner,
normaliseColour,
saveBanner,
} from '../branding.js';
import {
consumeRecoveryCode,
generateRecoveryCodes,
@@ -376,9 +383,11 @@ router.patch('/sites/:id', (req, res) => {
if (!site) return res.status(404).json({ error: 'Not found.' });
const badge = req.body?.badge || {};
const branding = req.body?.branding || {};
db.prepare(
`UPDATE sites SET name = ?, slug = ?, active = ?, badge_enabled = ?, badge_width_mm = ?,
badge_height_mm = ?, badge_show_photo = ?, badge_accent = ?, badge_note = ? WHERE id = ?`
badge_height_mm = ?, badge_show_photo = ?, badge_accent = ?, badge_note = ?,
colour_brand = ?, colour_signout = ?, colour_page = ?, banner_height = ? WHERE id = ?`
).run(
clean(req.body?.name ?? site.name, 100) || site.name,
req.body?.slug ? uniqueSlug(req.body.slug, site.id) : site.slug,
@@ -388,12 +397,60 @@ router.patch('/sites/:id', (req, res) => {
Math.min(200, Math.max(15, Number(badge.heightMm ?? site.badge_height_mm) || 54)),
badge.showPhoto !== undefined ? (badge.showPhoto ? 1 : 0) : site.badge_show_photo,
badge.accent !== undefined ? (badge.accent ? 1 : 0) : site.badge_accent,
badge.note !== undefined ? clean(badge.note, 120) || null : site.badge_note
badge.note !== undefined ? clean(badge.note, 120) || null : site.badge_note,
// A blank colour means "use the default", so it is stored as NULL rather than
// being quietly frozen at whatever the default happens to be today.
branding.brand !== undefined
? normaliseColour(branding.brand, null)
: site.colour_brand,
branding.signout !== undefined
? normaliseColour(branding.signout, null)
: site.colour_signout,
branding.page !== undefined ? normaliseColour(branding.page, null) : site.colour_page,
branding.bannerHeight !== undefined
? Math.min(200, Math.max(24, Number(branding.bannerHeight) || 64))
: site.banner_height
, site.id);
res.json(shapeSite(db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id)));
});
/* ------------------------------------------------------------- branding */
router.post('/sites/:id/banner', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Not found.' });
try {
assertSiteAllowed(req, site.id);
const name = saveBanner(site.id, req.body?.image);
if (site.banner_path) deleteBanner(site.banner_path);
db.prepare('UPDATE sites SET banner_path = ? WHERE id = ?').run(name, site.id);
res.json({ ok: true, url: `/api/branding/${site.id}/banner?t=${Date.now()}` });
} catch (err) {
res.status(err.status || 400).json({ error: err.message });
}
});
router.delete('/sites/:id/banner', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Not found.' });
try {
assertSiteAllowed(req, site.id);
} catch (err) {
return res.status(403).json({ error: err.message });
}
if (site.banner_path) deleteBanner(site.banner_path);
db.prepare('UPDATE sites SET banner_path = NULL WHERE id = ?').run(site.id);
res.json({ ok: true });
});
router.get('/sites/:id/banner', (req, res) => {
const site = db.prepare('SELECT banner_path FROM sites WHERE id = ?').get(req.params.id);
const abs = site && bannerAbsolutePath(site.banner_path);
if (!abs) return res.status(404).send('No banner set.');
res.sendFile(abs);
});
router.get('/sites/:id/badge-preview', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).send('Not found.');