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` 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`.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user