Fixed Pin

This commit is contained in:
jessikitty committed 2026-09-29 12:22:38 +10:00
1 parent ef0686a63e
commit 06b4616356
5 files changed
+74 -4

No files matched your search

@@ -179,3 +179,24 @@ Whole project created this session. Principal files:
- `brother_ql` 0.9.4 (PyPI) — `print --help` documents the `--red` requirement. - `brother_ql` 0.9.4 (PyPI) — `print --help` documents the `--red` requirement.
- Brother QL-820NWB specification: 12–62 mm media, 60.96 mm max print width, 300×300 dpi. - Brother QL-820NWB specification: 12–62 mm media, 60.96 mm max print width, 300×300 dpi.
- RFC 6238 (TOTP) — test vectors used to verify `src/auth.js`. - RFC 6238 (TOTP) — test vectors used to verify `src/auth.js`.
---
## Addendum — 2026-09-23, PIN reset bug
**Symptom:** after changing a recurring visitor's PIN, phone + PIN was rejected at the kiosk.
**Cause:** the Edit form carried a "PIN" field whose value `PATCH /admin/api/frequent/:id`
never read. The record saved, the field was silently discarded, and the admin handed out a PIN
the system had never stored. (`POST /admin/api/frequent/:id/pin`, the "New PIN" button, was
always correct — verified by reproduction.)
**Fix:** `PATCH` now honours a supplied `pin`: 4 digits, checked for collisions via
`allocatePin(pin, row.id)`, written to `pin_enc` and `pin_lookup`. Blank keeps the current PIN.
A malformed or already-used PIN is rejected with a message instead of ignored. The response
carries `pinChanged`, and the console shows the printable card when it is true. Saving a record
also clears `pin_attempts` for both the old and new phone number, so a changed number cannot
leave a stale lockout.
**Files:** `src/routes/admin.js` (PATCH handler), `public/js/admin.js` (edit modal shows the
current PIN, relabelled field, shows the card after a change), `public/css/admin.css`, `README.md`.
+10
View File
@@ -503,6 +503,16 @@ digits is only 10,000 combinations, so hashing would add nothing against anyone
of the database — the real protection is the lockout: five wrong PINs on a number locks it for of the database — the real protection is the lockout: five wrong PINs on a number locks it for
15 minutes. 15 minutes.
Two ways to change someone's PIN, and they now behave the same:
- **New PIN** on the recurring visitors list issues a random one.
- **Edit** shows the current PIN and has a box to type a replacement. Leave it blank to keep the
current one. A typed PIN must be 4 digits and not already in use; either problem is reported
rather than ignored.
Saving a record also clears any PIN lockout on that visitor, under both the old and the new
mobile number.
Four digits gives 10,000 combinations and each must be unique, so that is the ceiling on Four digits gives 10,000 combinations and each must be unique, so that is the ceiling on
simultaneous recurring visitors. Deactivating someone frees theirs. simultaneous recurring visitors. Deactivating someone frees theirs.
+6
View File
@@ -445,3 +445,9 @@ pre.raw {
border-radius: 3px; border-radius: 3px;
background: #fff; background: #fff;
} }
.pin-inline {
font-family: ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.12em;
color: var(--deep);
}
+10 -3
View File
@@ -502,7 +502,13 @@ function openRecurringModal(person = null) {
<select name="siteId">${siteOptions(person?.siteId)}</select></label> <select name="siteId">${siteOptions(person?.siteId)}</select></label>
<label class="modal-field"><span>Usually visiting</span> <label class="modal-field"><span>Usually visiting</span>
<select name="defaultHostId">${hostOptions(person?.defaultHostId)}</select></label> <select name="defaultHostId">${hostOptions(person?.defaultHostId)}</select></label>
${field('PIN (leave blank to generate one)', 'pin', '')} ${
editing
? `<label class="modal-field"><span>PIN — currently <b class="pin-inline">${esc(person.pin || '????')}</b></span>
<input name="pin" type="text" inputmode="numeric" maxlength="4" value=""
placeholder="Type 4 digits to change it, or leave blank"></label>`
: field('PIN (leave blank to generate one)', 'pin', '')
}
${field('Notes', 'notes', person?.notes)} ${field('Notes', 'notes', person?.notes)}
<h4 class="modal-section">Photo on file</h4> <h4 class="modal-section">Photo on file</h4>
${photoEditorMarkup(person)} ${photoEditorMarkup(person)}
@@ -519,8 +525,9 @@ function openRecurringModal(person = null) {
? await api(`/frequent/${person.id}`, { method: 'PATCH', body: payload }) ? await api(`/frequent/${person.id}`, { method: 'PATCH', body: payload })
: await api('/frequent', { method: 'POST', body: payload }); : await api('/frequent', { method: 'POST', body: payload });
loadRecurring(); loadRecurring();
if (editing) toast('Saved.'); if (!editing) showPin(saved.pin, saved.id);
else showPin(saved.pin, saved.id); else if (saved.pinChanged) showPin(saved.pin, saved.id);
else toast('Saved.');
} catch (err) { } catch (err) {
toast(err.message, true); toast(err.message, true);
} }
+27 -1
View File
@@ -941,6 +941,18 @@ router.patch('/frequent/:id', (req, res) => {
photoPath = null; photoPath = null;
} }
// A PIN typed into the edit form has to actually be set. It used to be
// accepted and ignored, so an admin would hand out a PIN the kiosk had never
// heard of. Blank means "leave the current one alone".
const requestedPin = String(req.body?.pin ?? '').trim();
let newPin = null;
if (requestedPin) {
if (!/^\d{4}$/.test(requestedPin)) {
throw new Error('A PIN must be exactly 4 digits. Leave it blank to keep the current one.');
}
newPin = allocatePin(requestedPin, row.id);
}
db.prepare( db.prepare(
`UPDATE frequent_visitors SET first_name = ?, last_name = ?, company = ?, phone = ?, email = ?, `UPDATE frequent_visitors SET first_name = ?, last_name = ?, company = ?, phone = ?, email = ?,
check_type = ?, check_number = ?, check_expiry = ?, default_host_id = ?, site_id = ?, check_type = ?, check_number = ?, check_expiry = ?, default_host_id = ?, site_id = ?,
@@ -954,7 +966,21 @@ router.patch('/frequent/:id', (req, res) => {
req.body?.active !== undefined ? (req.body.active ? 1 : 0) : row.active, req.body?.active !== undefined ? (req.body.active ? 1 : 0) : row.active,
row.id row.id
); );
res.json(shapeFrequent(db.prepare('SELECT * FROM frequent_visitors WHERE id = ?').get(row.id), true));
if (newPin) {
db.prepare(
"UPDATE frequent_visitors SET pin_enc = ?, pin_lookup = ?, updated_at = datetime('now') WHERE id = ?"
).run(encryptPin(newPin), pinLookup(newPin), row.id);
}
// Attempt counters are keyed by phone number, so a changed number would
// otherwise leave a stale lockout behind under the old one.
db.prepare('DELETE FROM pin_attempts WHERE phone IN (?, ?)').run(row.phone, v.phone);
res.json({
...shapeFrequent(db.prepare('SELECT * FROM frequent_visitors WHERE id = ?').get(row.id), true),
pinChanged: Boolean(newPin),
});
} catch (err) { } catch (err) {
res.status(400).json({ error: err.message }); res.status(400).json({ error: err.message });
} }