diff --git a/Claude.md b/Claude.md index c42cbc1..ecef094 100644 --- a/Claude.md +++ b/Claude.md @@ -586,8 +586,19 @@ source of the bilingual labels echoed back to the page. **Employee ID is numeric only** — `inputmode="numeric"`, `pattern="[0-9]*"`, an `input` listener stripping non-digits (paste / autofill), a re-strip at submit, and a digits-only guard before the POST. A `localStorage` value stored before this rule -(e.g. `1234SP`) is cleaned to `1234` on auto-fill. There is **no** `maxlength` cap — -capping at 4 would break any 5-digit employee ID. +(e.g. `1234SP`) is cleaned to `1234` on auto-fill. + +**Employee ID is at most 4 digits (Sept 15, 2026, user decision)** — `maxlength="4"`, plus +`.slice(0, 4)` in both `input` listeners (script-set values ignore `maxlength`), a bilingual +guard on both submit paths, and an HTTP 400 in `qr_checkin` when the base ID has more than +4 digits; `last-work-type` stays quiet for longer IDs. Shorter IDs are still accepted. Longer +IDs are **refused, never truncated** (a cut-down ID is a different employee), and a stored ID +longer than 4 digits is **not** auto-filled. **An employee with a 5+ digit ID cannot check in** — +if that changes, raise all four together: the `maxlength` attribute, `EMPLOYEE_ID_MAX_DIGITS` +(`qr_destination.html`), `QR_EMPLOYEE_ID_MAX_DIGITS` (`qr_destination.js`) and +`CHECKIN_EMPLOYEE_ID_MAX_DIGITS` (`routes/qr_codes.py`). The two JS constants have different +names on purpose: both scripts share the page's global scope, and a duplicate top-level +`const` is a SyntaxError that would break the whole page. ### Type of Work — Anti-Mistake Measures (Sept 2026) @@ -1246,6 +1257,14 @@ it (the workers share no pub/sub). | `static/js/attendance_report.js` | **Security:** `createTableRow()` put raw device / address / name text into `innerHTML`. Device comes from the check-in User-Agent, so a crafted UA could inject markup into the report — every value is now escaped | | — | Verified: filter helper produces identical SQL + params to the previous inline block for 180 input combinations; `_live_record_payload()` matches what `loadTableData()` reads from the Jinja-rendered `
` for 20 row variants; the real `attendance_report.js` driven with a fake DOM/timers/fetch passes 26 checks (insert, de-dup, page + sort kept, hidden-tab pause, no overlap, backoff + cap, stop on logout, escaping). Not yet exercised against a live MySQL server or a real browser | +### Set 21 — Check-In Employee ID Limited to 4 Digits (Sept 15, 2026) +| File | Change | +|---|---| +| `templates/qr_destination.html` | `maxlength="4"`; `EMPLOYEE_ID_MAX_DIGITS`; input listener cuts to 4 digits; stored IDs over 4 digits not auto-filled; submit guard with bilingual message | +| `static/js/qr_destination.js` | `QR_EMPLOYEE_ID_MAX_DIGITS`; same cut in `initializeForm()`; `loadLastStaffId()` ignores longer IDs; guards in `handleFormSubmit()` and `submitCheckin()` | +| `routes/qr_codes.py` | `CHECKIN_EMPLOYEE_ID_MAX_DIGITS`; `qr_checkin` rejects a base ID over 4 digits (HTTP 400, bilingual); `qr_last_work_type` returns no suggestion for longer IDs | +| — | "Up to 4 digits" chosen over "exactly 4": 1–3 digit IDs still check in. Supersedes the earlier "no `maxlength` cap" note in §11 | + --- ## 21. Infrastructure & Deployment diff --git a/routes/qr_codes.py b/routes/qr_codes.py index 3b3de21..85a8a6a 100644 --- a/routes/qr_codes.py +++ b/routes/qr_codes.py @@ -51,6 +51,11 @@ bp = Blueprint('qr_codes', __name__) # PT is accepted for backward compatibility with IDs created before the dropdown. VALID_CHECKIN_WORK_TYPES = ('SP', 'PW', 'PT', 'C') +# Employee IDs entered on the check-in page are at most 4 digits. Keep in sync +# with the maxlength on #employee_id, EMPLOYEE_ID_MAX_DIGITS in +# qr_destination.html and QR_EMPLOYEE_ID_MAX_DIGITS in qr_destination.js. +CHECKIN_EMPLOYEE_ID_MAX_DIGITS = 4 + # Bilingual labels for each work type, echoed back to the check-in page so the # submit button, the success card, and the check-out reminder all name the type # the same way the dropdown does. Keyed by code; '' is Regular (no code stored). @@ -739,6 +744,21 @@ def qr_checkin(qr_url): # Get and validate employee ID employee_id = request.form.get('employee_id', '').strip() + # At most 4 digits — counted on the base ID, so an old-style typed + # suffix ("1234SP") from a page cached before the numeric-only rule + # still passes. Refused, never truncated: a shorter ID is another person. + if employee_id: + base_for_length, _ = parse_employee_id_for_work_type(employee_id) + if len(re.sub(r'\D', '', base_for_length)) > CHECKIN_EMPLOYEE_ID_MAX_DIGITS: + logger_handler.logger.warning( + f"Check-in rejected: employee ID '{employee_id}' has more than " + f"{CHECKIN_EMPLOYEE_ID_MAX_DIGITS} digits (QR {qr_url})" + ) + return jsonify({ + 'success': False, + 'message': 'Employee ID must be 4 digits or fewer. / El ID de empleado debe tener 4 dígitos o menos.' + }), 400 + # --- ADDED: type of work selected on the check-in page --- # The employee enters a numeric ID and picks a work type; the code is # appended to the ID so the stored value keeps the existing storage @@ -1234,8 +1254,8 @@ def qr_last_work_type(qr_url): return _no_store_json(empty) employee_id = request.args.get('employee_id', '').strip() - # The check-in page allows digits only; anything else cannot be matched. - if not employee_id.isdigit(): + # The check-in page allows up to 4 digits; anything else cannot be matched. + if not employee_id.isdigit() or len(employee_id) > CHECKIN_EMPLOYEE_ID_MAX_DIGITS: return _no_store_json(empty) selected_location_name = request.args.get('selected_location_name', '').strip() diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index 6a45df8..592043b 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -7,6 +7,11 @@ let isSubmitting = false; let currentTime = new Date(); +// Employee IDs are at most 4 digits. Keep in sync with the maxlength on +// #employee_id, EMPLOYEE_ID_MAX_DIGITS in qr_destination.html and +// CHECKIN_EMPLOYEE_ID_MAX_DIGITS in routes/qr_codes.py. +const QR_EMPLOYEE_ID_MAX_DIGITS = 4; + // Camera verification variables let cameraStream = null; let capturedPhotoData = null; @@ -97,7 +102,8 @@ function loadLastStaffId() { try { const saved = localStorage.getItem("qr_last_staff_id"); const digitsOnly = (saved || "").replace(/[^0-9]/g, ""); - if (digitsOnly.length >= 2) { + // Longer stored IDs are not filled in: cutting them would be another employee + if (digitsOnly.length >= 2 && digitsOnly.length <= QR_EMPLOYEE_ID_MAX_DIGITS) { return digitsOnly; } return null; @@ -135,9 +141,9 @@ function initializeForm() { // Add real-time Employee ID validation const employeeIdInput = document.getElementById("employee_id"); if (employeeIdInput) { - // Employee ID is numeric only — strip anything else as it is typed + // Employee ID is numeric only, at most 4 digits — strip anything else as it is typed employeeIdInput.addEventListener("input", function () { - const digitsOnly = this.value.replace(/[^0-9]/g, ""); + const digitsOnly = this.value.replace(/[^0-9]/g, "").slice(0, QR_EMPLOYEE_ID_MAX_DIGITS); if (this.value !== digitsOnly) { this.value = digitsOnly; } @@ -275,6 +281,15 @@ function proceedWithCheckin() { return; } + // Refuse rather than truncate: a cut-down ID is a different employee + if (employeeId.length > QR_EMPLOYEE_ID_MAX_DIGITS) { + showCustomStatusMessage( + "Employee ID must be 4 digits or fewer / El ID de empleado debe tener 4 dígitos o menos", + "error" + ); + return; + } + // Save the staff ID for future use saveLastStaffId(employeeId); @@ -324,6 +339,16 @@ function submitCheckin() { return; } + if (employeeId.length > QR_EMPLOYEE_ID_MAX_DIGITS) { + showCustomStatusMessage( + "Employee ID must be 4 digits or fewer / El ID de empleado debe tener 4 dígitos o menos", + "error" + ); + isSubmitting = false; + updateSubmitButton(false); + return; + } + const workTypeField = document.getElementById("work_type"); const workType = workTypeField ? workTypeField.value.trim() : ""; diff --git a/templates/qr_destination.html b/templates/qr_destination.html index 4a74b53..ce8c79a 100644 --- a/templates/qr_destination.html +++ b/templates/qr_destination.html @@ -1203,6 +1203,7 @@ autocomplete="off" inputmode="numeric" pattern="[0-9]*" + maxlength="4" /> @@ -1666,6 +1667,12 @@ loadOpenCheckInWorkType(); } + // Employee IDs are at most 4 digits. Keep in sync with the maxlength on + // #employee_id, QR_EMPLOYEE_ID_MAX_DIGITS in qr_destination.js and + // CHECKIN_EMPLOYEE_ID_MAX_DIGITS in routes/qr_codes.py. (Distinct names: + // both scripts share the page's global scope.) + const EMPLOYEE_ID_MAX_DIGITS = 4; + // FIXED Employee ID auto-fill functionality function initializeEmployeeIdAutoFill() { const employeeIdInput = document.getElementById("employee_id"); @@ -1681,7 +1688,9 @@ localStorage.getItem("qr_last_employee_id") || "" ).replace(/[^0-9]/g, ""); - if (lastEmployeeId !== "") { + // A stored ID longer than the limit is not filled in: cutting it + // down would silently check in a different employee. + if (lastEmployeeId !== "" && lastEmployeeId.length <= EMPLOYEE_ID_MAX_DIGITS) { employeeIdInput.value = lastEmployeeId; // Visual feedback that it's auto-filled @@ -1702,10 +1711,11 @@ function attachEmployeeIdListeners(employeeIdInput) { if (!employeeIdInput) return; - // Employee ID is numeric only — strip anything else as it is typed - // (covers paste, autofill and keyboards that ignore inputmode). + // Employee ID is numeric only, at most 4 digits — strip anything else as + // it is typed (covers paste, autofill and keyboards that ignore + // inputmode; maxlength alone does not limit values set by script). employeeIdInput.addEventListener("input", function () { - const digitsOnly = this.value.replace(/[^0-9]/g, ""); + const digitsOnly = this.value.replace(/[^0-9]/g, "").slice(0, EMPLOYEE_ID_MAX_DIGITS); if (this.value !== digitsOnly) { this.value = digitsOnly; } @@ -1914,6 +1924,15 @@ return; } + // Refuse rather than truncate: a cut-down ID is a different employee + if (employeeId.length > EMPLOYEE_ID_MAX_DIGITS) { + showStatusMessage( + "Employee ID must be 4 digits or fewer / El ID de empleado debe tener 4 dígitos o menos", + "error" + ); + return; + } + // Save Employee ID to localStorage for next time try { localStorage.setItem("qr_last_employee_id", employeeId);