Three controller changes in one commit (tight code coupling):
1. /fp/tablet/request_reset_code (Task 2) — generates 4-digit code,
emails it, returns masked_email. Specific error codes for the
frontend to switch on (no_email + manager_name, rate_limited +
wait_minutes, user_not_found, no_role, inactive). Shop-branch
role check matches existing _check_credentials per Rule 13l + 23
(all_group_ids transitive — Owners reach Technician through
implication).
2. /fp/tablet/verify_reset_code (Task 3) — verifies the emailed
code, on success mints a 5-min HMAC reset_token. Error responses
are specific (no_active_code / expired / too_many_attempts /
wrong_code with attempts_left).
3. set_pin extended to accept reset_token (Task 4) — three auth
paths now: old_pin (existing), reset_token (new), or neither
(existing — only for users with no current hash). reset_token
path is the only one that operates on a user OTHER than env.user;
token proves the legit user just verified their email.
Failure audit reuses existing failed_unlock event_type with a notes
field describing the reset-code-specific reason. Success audit uses
the new pin_reset_requested / pin_reset_code_verified /
pin_set_after_reset event_type values.
_mask_email helper added for the no-email-on-file edge case.
3 more tests cover: valid token roundtrip + set_pin, expired token
rejection, and lockout-cleared-on-reset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>