Public Access
Fixed Pin
This commit is contained in:
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-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`.
|
||||
|
||||
---
|
||||
|
||||
## 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`.
|
||||
@@ -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
|
||||
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
|
||||
simultaneous recurring visitors. Deactivating someone frees theirs.
|
||||
|
||||
|
||||
@@ -445,3 +445,9 @@ pre.raw {
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pin-inline {
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--deep);
|
||||
}
|
||||
+10
-3
@@ -502,7 +502,13 @@ function openRecurringModal(person = null) {
|
||||
<select name="siteId">${siteOptions(person?.siteId)}</select></label>
|
||||
<label class="modal-field"><span>Usually visiting</span>
|
||||
<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)}
|
||||
<h4 class="modal-section">Photo on file</h4>
|
||||
${photoEditorMarkup(person)}
|
||||
@@ -519,8 +525,9 @@ function openRecurringModal(person = null) {
|
||||
? await api(`/frequent/${person.id}`, { method: 'PATCH', body: payload })
|
||||
: await api('/frequent', { method: 'POST', body: payload });
|
||||
loadRecurring();
|
||||
if (editing) toast('Saved.');
|
||||
else showPin(saved.pin, saved.id);
|
||||
if (!editing) showPin(saved.pin, saved.id);
|
||||
else if (saved.pinChanged) showPin(saved.pin, saved.id);
|
||||
else toast('Saved.');
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
|
||||
+27
-1
@@ -941,6 +941,18 @@ router.patch('/frequent/:id', (req, res) => {
|
||||
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(
|
||||
`UPDATE frequent_visitors SET first_name = ?, last_name = ?, company = ?, phone = ?, email = ?,
|
||||
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,
|
||||
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) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user