Skip to content

feat(auth): fast cashier switch by PIN - #1392

Merged
khaira777 merged 8 commits into
mainfrom
fm/flocafe-1338-pin-quick-switch
Oct 28, 2026
Merged

khaira777 merged 8 commits into
mainfrom
fm/flocafe-1338-pin-quick-switch

Conversation

@khaira777

@khaira777 khaira777 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1338

Intent

Implement and verify issue #1338 (feat(auth): PIN quick user switch with user picker (PIN for all roles, approvals stay owner/manager)) and open a PR targeting main without merging it:

  • Problem: An owner and a cashier sharing one till have to fully log out and back in with email and password.
  • Decisions:
  • Reuse the existing per-user PIN (4-6 digits, bcrypt pin_hash) and allow it for every staff role (main/routes/staff.ts ~L314). It doubles as the sign-in PIN.
  • Only owner and manager PINs approve actions, so a cashier, server, or chef PIN must never satisfy an approval check.
  • Quick switch with a user picker: when the POS is locked or logged out, show active users with a PIN (id, name, role only) as tiles. Tap a user, then enter that user's PIN. The PIN is checked only against the selected user (POST /api/auth/pin-login taking { user_id, pin }), never by scanning all users, so two users may share the same PIN.
  • Only one user is signed in on a device at a time. Switching replaces the token. Email and password sign-in stays available.
  • Add a "Lock" action in the topbar, plus an optional idle auto-lock store setting (off by default; choices 1, 5, 15, or 30 minutes).
  • A user without a PIN does not get a quick-switch tile and uses email and password.
  • Reuse the install-wide login rate limiting (fix(auth): enforce install-wide login attempt rate limiting across servers #1199) per user, and audit each PIN sign-in.
  • The open cash shift is not touched by switching. Keep the unfinished POS cart with the terminal across lock/switch; after switching, recheck the new user's permissions, require fresh authorization for privileged adjustments, clear previous tokens/PINs/approval state/privileged modal state, and ignore stale async callbacks from the previous session.
  • Hardening (must do): audit every approval path (refund, item void, discount, order-status and kitchen-delivery overrides, charge waiver) to confirm it enforces the owner/manager role, not just a matching PIN. Add a regression test that a cashier with a PIN is rejected on each.
  • Wording: rename user-facing PIN strings so there is one term ("PIN") for staff and "Staff Approval PIN" only where approval is meant. Fix kitchenDeliveryOverridePrompt, which still says "Manager PIN", across all 24 locales.
  • Docs: update docs/architecture/authentication-and-authorization.md and docs/reference/roles-and-permissions.md.

What Changed

  • Allow 4-6 digit PIN configuration for all staff roles (owner, manager, cashier, server, chef) and add GET /api/auth/pin-users and POST /api/auth/pin-login with per-account and per-IP login rate limiting, serialized PIN verification, and auth_audit_log recording (event_type = 'pin_login').
  • Add a quick-switch user tile picker on the sign-in screen, topbar and sidebar Lock actions, and an optional auto_lock_minutes idle auto-lock store setting (0, 1, 5, 15, or 30 minutes) that revokes the active session token while preserving the unfinished POS cart and clearing privileged cart adjustments, modal state, and stale async callbacks across session boundaries.
  • Enforce active owner and manager role filtering on every Staff Approval PIN candidate query and in-transaction re-validation (refunds, item voids, order and item discounts, order-status overrides, kitchen-delivery overrides, and charge waivers), and update PIN vs. Staff Approval PIN translations and documentation across all 24 locales.

Risk Assessment

✅ Low: All 56 changed files were reviewed against the issue #1338 acceptance criteria and repository invariants. Per-user PIN assignment is opened to all staff roles while all approval and override paths (approvers list, refunds, item voids, order/item/bill discounts, order-status overrides, kitchen-delivery overrides, and charge waivers) strictly enforce active owner/manager role eligibility both during async pre-verification and inside write transactions. Selected-user PIN login serializes verification per target account, enforces both per-IP+user and cross-IP per-account rate-limit lockouts, and records auth_audit_log events. Terminal lock and user switching preserve unsubmitted POS cart items while clearing privileged cart adjustments, modal state, and stale async callbacks.

Testing

Built the backend and static desktop frontend, ran targeted backend integration suites (test:pin-quick-switch, test:staff-authz, test:staff-approvers-directory, test:discount-kitchen-approver-identity, test:cancel-override, test:refunds, test:translations, test:menu-surface, test:schema-health, test:migration-registry, and backup-restore-production.test.ts) plus Playwright E2E suites (e2e/pin-quick-switch.spec.ts, e2e/approval-manager-selection.spec.ts, and e2e/database-export-import.spec.ts), and drove 5 live end-to-end scenarios in Chromium against the running server with 8 screenshots and structured JSON evidence captured in the evidence directory; all checks and scenarios passed.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Owner configures a 4-6 digit PIN for an operational staff role (cashier) in Staff management, sees updated PIN wording, and GET /api/auth/pin-users returns only active PIN users (id, name, role only)… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-1-staff-pin-for-cashier.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/live-verification-ev…
Operator locks the POS from the topbar and quick-switches via the user tile picker to a cashier sharing the same PIN as the owner, revoking the previous token, keeping the open cash shift and unfinish… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-2-login-pin-user-picker.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-2-login-pin…
Adversarial: Cashier with a valid PIN and granted route permissions attempts to self-approve bill discount, order discount, item discount, in-progress item void, order cancel/complete overrides, charg… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-3-approval-modal-staff-approval-pin.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/live-ver…
Adversarial: Entering 5 wrong PINs for a selected user locks out that user with HTTP 429 (even when the 6th attempt uses the correct PIN or a rotated IP) and records pin_login events in auth_audit_log… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-4-pin-login-lockout.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/live-verification-eviden…
Owner configures Idle Auto-Lock in Settings > POS (default Off; choices 1, 5, 15, 30 min; invalid value 10 rejected with 400), and after the configured inactivity period elapses the terminal automatic… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-5-settings-auto-lock-dropdown.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-5-idl…

Scenario 1: Staff modal configuring 4-6 digit PIN for Cashier role with updated PIN hint
Scenario 2a: Login page Quick Switch user tile picker alongside email/password sign-in
Scenario 2b: Login page Quick Switch PIN keypad for selected Cashier tile
Scenario 2c: POS after quick-switching to Cashier with unfinished cart and open cash shift preserved and privileged adjustments cleared
Scenario 3: Kitchen Delivery Override modal showing updated Staff Approval PIN wording and approvers list excluding Cashier
Scenario 4: Per-user PIN sign-in lockout message after 5 wrong PIN attempts for Cashier
Scenario 5a: Settings > POS Workflow Idle Auto-Lock dropdown configured to 1 min
Scenario 5b: Terminal automatically locked and redirected to Quick Switch login after 1 minute of inactivity

Evidence: Live end-to-end verification API and state evidence summary
{
  "scenario1_pinUsers": [
    {
      "id": "e2e-owner",
      "name": "E2E owner",
      "role": "owner"
    },
    {
      "id": "e2e-manager",
      "name": "E2E manager",
      "role": "manager"
    },
    {
      "id": "cf9953be-bb1f-4d55-baa3-53456ae5877b",
      "name": "Live Switch Cashier",
      "role": "cashier"
    }
  ],
  "scenario2_quickSwitch": {
    "revokedPreviousTokenStatus": 401,
    "switchedUser": {
      "id": "cf9953be-bb1f-4d55-baa3-53456ae5877b",
      "name": "Live Switch Cashier",
      "email": "live-switch-cashier@flo.local",
      "role": "cashier",
      "category_ids": []
    },
    "openShiftPreserved": {
      "id": 1,
      "status": "open"
    },
    "privilegedPriceOverrideCleared": true,
    "privilegedChargeWaiverCleared": true
  },
  "scenario3_approvalHardening": {
    "approversReturned": [
      {
        "id": "e2e-manager",
        "name": "E2E manager",
        "role": "manager",
        "is_active": 1,
        "has_pin": 1
      },
      {
        "id": "e2e-owner",
        "name": "E2E owner",
        "role": "owner",
        "is_active": 1,
        "has_pin": 1
      }
    ],
    "cashierPinRejectionStatuses": {
      "billDiscount": 403,
      "orderDiscount": 403,
      "itemDiscount": 403,
      "itemVoid": 403,
      "orderCancelOverride": 403,
      "orderCompleteOverride": 403,
      "chargeWaiver": 403,
      "kitchenDeliveryOverride": 403,
      "refundWithCashierPin": 403
    },
    "ownerApprovedRefundStatus": 201
  },
  "scenario4_rateLimiting": {
    "cashierAttempts": [
      {
        "attempt": 1,
        "status": 401,
        "attempts_remaining": 4
      },
      {
        "attempt": 2,
        "status": 401,
        "attempts_remaining": 3
      },
      {
        "attempt": 3,
        "status": 401,
        "attempts_remaining": 2
      },
      {
        "attempt": 4,
        "status": 401,
        "attempts_remaining": 1
      },
      {
        "attempt": 5,
        "status": 401,
        "attempts_remaining": 0
      },
      {
        "attempt": 6,
        "status": 429
      }
    ],
    "ownerSignInOnSameTerminalStatus": 200
  },
  "scenario5_idleAutoLock": {
    "defaultValue": "0",
    "allowedOptions": [
      "0",
      "1",
      "5",
      "15",
      "30"
    ],
    "invalidValue10Status": 400,
    "saved1MinuteStatus": 200,
    "autoLockedToLogin": "http://127.0.0.1:43021/auth/login/"
  }
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Owner configures a 4-6 digit PIN for an operational staff role (cashier) in Staff management, sees updated PIN wording, and GET /api/auth/pin-users returns only active PIN users (id, name, role only)… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-1-staff-pin-for-cashier.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/live-verification-ev…
Operator locks the POS from the topbar and quick-switches via the user tile picker to a cashier sharing the same PIN as the owner, revoking the previous token, keeping the open cash shift and unfinish… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-2-login-pin-user-picker.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-2-login-pin…
Adversarial: Cashier with a valid PIN and granted route permissions attempts to self-approve bill discount, order discount, item discount, in-progress item void, order cancel/complete overrides, charg… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-3-approval-modal-staff-approval-pin.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/live-ver…
Adversarial: Entering 5 wrong PINs for a selected user locks out that user with HTTP 429 (even when the 6th attempt uses the correct PIN or a rotated IP) and records pin_login events in auth_audit_log… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-4-pin-login-lockout.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/live-verification-eviden…
Owner configures Idle Auto-Lock in Settings > POS (default Off; choices 1, 5, 15, 30 min; invalid value 10 rejected with 400), and after the configured inactivity period elapses the terminal automatic… ✅ pass live ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-5-settings-auto-lock-dropdown.png; ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF/scenario-5-idl…
  • npm run test:pin-quick-switch
  • npm run test:staff-authz
  • npm run test:staff-approvers-directory
  • npm run test:discount-kitchen-approver-identity
  • npm run test:translations
  • node tests/run-electron-node-test.cjs tests/backup-restore-production.test.ts
  • npm run test:cancel-override
  • npm run test:refunds
  • npm run test:menu-surface
  • npm run test:schema-health
  • npm run test:migration-registry
  • cd frontend && npx playwright test e2e/pin-quick-switch.spec.ts e2e/approval-manager-selection.spec.ts e2e/database-export-import.spec.ts --project=chromium
  • Live Chromium end-to-end verification of all 5 scenarios against node tests/e2e-server.cjs with screenshots and JSON evidence saved under ~/.no-mistakes/evidence/01M62C1F6QJAPNHG450HF38KFF
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add PIN quick switching and terminal lock with protected approvals

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Let staff switch users with individual PINs while reserving approvals for active owners and
 managers.
• Add manual and optional idle locking without losing unfinished carts or retaining privileged
 adjustments.
• Add auth auditing, regression tests, API documentation, and translations across 24 locales.
Diagram

graph TD
  Staff["Staff PINs"] --> Auth["PIN Auth API"] --> Picker["User Picker"] --> Session["Session Store"] --> Cart["POS Cart"]
  Settings["Lock Setting"] --> Lock["Lock Controls"] --> Session
  Staff --> Approval["Approval Routes"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize approval PIN verification
  • ➕ A shared service could enforce role, active-status, permission, and rate-limit rules consistently across endpoints.
  • ➖ Refactoring established order and billing transaction flows would broaden this security-sensitive change and require more regression testing.

Recommendation: Keep the PR's explicit approval checks for this feature: they constrain candidates and revalidate before writes without redesigning existing workflows. Consider a shared verifier separately if more approval paths are added.

Files changed (53) +2729 / -324

Enhancement (11) +742 / -88
layout.tsxLock the dashboard after configured inactivity +58/-1

Lock the dashboard after configured inactivity

• Loads the auto-lock duration and resets a terminal-lock timer on user activity.

frontend/src/app/(dashboard)/layout.tsx

page.tsxClose sensitive POS controls when users change +13/-0

Close sensitive POS controls when users change

• Clears cached checkout authorization and closes payment, prepaid, drawer, and shift modals on a user change.

frontend/src/app/(dashboard)/pos/page.tsx

page.tsxExpose PIN configuration for every staff role +25/-28

Expose PIN configuration for every staff role

• Keeps the PIN field available when changing roles instead of limiting it to owners and managers.

frontend/src/app/(dashboard)/staff/page.tsx

page.tsxAdd user tiles and PIN keypad to login +297/-30

Add user tiles and PIN keypad to login

• Displays PIN-enabled staff, supports targeted PIN sign-in and failure feedback, and retains password login.

frontend/src/app/auth/login/page.tsx

MenuActionHandler.tsxWire desktop lock menu actions +5/-0

Wire desktop lock menu actions

• Routes lock menu commands to the auth store's lock action.

frontend/src/components/layout/MenuActionHandler.tsx

Sidebar.tsxAdd sidebar Lock action +14/-1

Add sidebar Lock action

• Exposes terminal locking in the account menu.

frontend/src/components/layout/Sidebar.tsx

PosTopbar.tsxAdd POS topbar Lock button +14/-0

Add POS topbar Lock button

• Provides a direct manual lock action on the POS screen.

frontend/src/components/pos/PosTopbar.tsx

auth.tsManage lock and targeted PIN sessions +96/-10

Manage lock and targeted PIN sessions

• Adds PIN login and lock actions, session-boundary notifications, and guards against stale auth refresh callbacks.

frontend/src/store/auth.ts

cart.tsRetain cart items while clearing privileged changes +33/-0

Retain cart items while clearing privileged changes

• Removes price overrides and charge waivers at session boundaries, merging cart lines that become identical.

frontend/src/store/cart.ts

auth.tsServe PIN users and targeted PIN login +180/-1

Serve PIN users and targeted PIN login

• Lists active PIN-enabled users and verifies a selected user's PIN with per-user failed-attempt limits, audit records, and token issuance.

main/routes/auth.ts

staff.tsAllow sign-in PINs for operational staff +7/-17

Allow sign-in PINs for operational staff

• Removes the owner/manager-only PIN restriction and preserves existing PINs when staff change roles.

main/routes/staff.ts

Bug fix (4) +182 / -30
CartPanel.tsxScope price-override dialog to its user +15/-5

Scope price-override dialog to its user

• Prevents a price-override modal opened by one user from remaining actionable after a user switch.

frontend/src/components/pos/CartPanel.tsx

api.tsIgnore unauthorized responses from superseded sessions +7/-0

Ignore unauthorized responses from superseded sessions

• Stops a stale request bearing an earlier token from clearing a newly established session.

frontend/src/lib/api.ts

bills.tsRestrict bill approvals and authorize charge waivers +133/-11

Restrict bill approvals and authorize charge waivers

• Filters PIN approvers to active owners and managers across bill paths. Adds approval, rate limiting, transaction-time revalidation, and attribution for charge waivers.

main/routes/bills.ts

orders.tsRestrict order approval PIN candidates +27/-14

Restrict order approval PIN candidates

• Applies owner/manager eligibility to order status, discount, and item-void approval paths, including transaction revalidation.

main/routes/orders.ts

Tests (5) +1276 / -70
database-export-import.spec.tsAlign export fixture with schema version 119 +1/-1

Align export fixture with schema version 119

• Updates the fixture's schema version after the auth audit migration.

frontend/e2e/database-export-import.spec.ts

pin-quick-switch.spec.tsExercise lock-to-PIN-switch checkout flow +166/-0

Exercise lock-to-PIN-switch checkout flow

• Covers locking from POS, selecting a cashier tile, preserving the unfinished cart, and completing payment.

frontend/e2e/pin-quick-switch.spec.ts

backup-restore-production.test.tsExpect migration through schema version 119 +2/-2

Expect migration through schema version 119

• Adjusts the backup-recovery assertion for the new auth audit migration.

tests/backup-restore-production.test.ts

pin-quick-switch.test.tsCover PIN auth, approvals, locking, and cart state +851/-0

Cover PIN auth, approvals, locking, and cart state

• Tests shared PINs, picker eligibility, failed-attempt auditing, approval rejection and acceptance, auto-lock validation, and frontend session-boundary behavior.

tests/pin-quick-switch.test.ts

staff-authz.test.tsVerify operational staff retain sign-in PINs +256/-67

Verify operational staff retain sign-in PINs

• Changes staff authorization expectations to permit operational-role PINs, preserve a PIN after demotion, and exclude those users from approvers.

tests/staff-authz.test.ts

Documentation (27) +364 / -134
authentication-and-authorization.mdDocument quick switching and approval boundaries +36/-5

Document quick switching and approval boundaries

• Explains all-role sign-in PINs, targeted login, auditing, terminal locking, and owner/manager-only approvals.

docs/architecture/authentication-and-authorization.md

api.mdDocument PIN auth and auto-lock APIs +10/-8

Document PIN auth and auto-lock APIs

• Adds the PIN directory and login contracts, auto-lock setting, and charge-waiver approval requirements.

docs/reference/api.md

roles-and-permissions.mdDistinguish sign-in PINs from approval PINs +6/-1

Distinguish sign-in PINs from approval PINs

• Clarifies that any staff member may have a sign-in PIN, but only active owners and managers may approve protected actions.

docs/reference/roles-and-permissions.md

ar.jsonLocalize PIN switching and locking in Arabic +13/-5

Localize PIN switching and locking in Arabic

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/ar.json

bn.jsonLocalize PIN switching and locking in Bengali +13/-5

Localize PIN switching and locking in Bengali

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/bn.json

de.jsonLocalize PIN switching and locking in German +13/-5

Localize PIN switching and locking in German

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/de.json

en.jsonAdd English PIN-switch and lock messages +13/-5

Add English PIN-switch and lock messages

• Adds picker, terminal-lock, and auto-lock copy and distinguishes quick sign-in from approval use.

frontend/src/lib/i18n/messages/en.json

es.jsonLocalize PIN switching and locking in Spanish +13/-5

Localize PIN switching and locking in Spanish

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/es.json

fa.jsonLocalize PIN switching and locking in Persian +13/-5

Localize PIN switching and locking in Persian

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/fa.json

fil.jsonLocalize PIN switching and locking in Filipino +13/-5

Localize PIN switching and locking in Filipino

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/fil.json

fr.jsonLocalize PIN switching and locking in French +13/-5

Localize PIN switching and locking in French

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/fr.json

hi.jsonLocalize PIN switching and locking in Hindi +13/-5

Localize PIN switching and locking in Hindi

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/hi.json

id.jsonLocalize PIN switching and locking in Indonesian +13/-5

Localize PIN switching and locking in Indonesian

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/id.json

it.jsonLocalize PIN switching and locking in Italian +13/-5

Localize PIN switching and locking in Italian

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/it.json

ja.jsonLocalize PIN switching and locking in Japanese +13/-5

Localize PIN switching and locking in Japanese

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/ja.json

ko.jsonLocalize PIN switching and locking in Korean +13/-5

Localize PIN switching and locking in Korean

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/ko.json

ne.jsonLocalize PIN switching and locking in Nepali +13/-5

Localize PIN switching and locking in Nepali

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/ne.json

nl.jsonLocalize PIN switching and locking in Dutch +13/-5

Localize PIN switching and locking in Dutch

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/nl.json

pt.jsonLocalize PIN switching and locking in Portuguese +13/-5

Localize PIN switching and locking in Portuguese

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/pt.json

ru.jsonLocalize PIN switching and locking in Russian +13/-5

Localize PIN switching and locking in Russian

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/ru.json

sq.jsonLocalize PIN switching and locking in Albanian +13/-5

Localize PIN switching and locking in Albanian

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/sq.json

th.jsonLocalize PIN switching and locking in Thai +13/-5

Localize PIN switching and locking in Thai

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/th.json

tr.jsonLocalize PIN switching and locking in Turkish +13/-5

Localize PIN switching and locking in Turkish

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/tr.json

ur.jsonLocalize PIN switching and locking in Urdu +13/-5

Localize PIN switching and locking in Urdu

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/ur.json

vi.jsonLocalize PIN switching and locking in Vietnamese +13/-5

Localize PIN switching and locking in Vietnamese

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/vi.json

zh-tw.jsonLocalize PIN switching and locking in Traditional Chinese +13/-5

Localize PIN switching and locking in Traditional Chinese

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/zh-tw.json

zh.jsonLocalize PIN switching and locking in Simplified Chinese +13/-5

Localize PIN switching and locking in Simplified Chinese

• Adds picker and auto-lock text and revises PIN approval wording.

frontend/src/lib/i18n/messages/zh.json

Other (6) +165 / -2
page.tsxAdd idle auto-lock setting control +36/-1

Add idle auto-lock setting control

• Lets administrators select off, 1, 5, 15, or 30 minutes and persists the choice with rollback on failure.

frontend/src/app/(dashboard)/settings/page.tsx

pos-settings.tsTrack the normalized auto-lock duration +19/-0

Track the normalized auto-lock duration

• Adds typed auto-lock state and hydrates it from backend settings.

frontend/src/store/pos-settings.ts

session-events.tsPublish authentication session boundaries +22/-0

Publish authentication session boundaries

• Introduces a listener mechanism so other stores can react to lock, login, PIN login, and logout.

frontend/src/store/session-events.ts

db.tsPersist authentication audit events +45/-0

Persist authentication audit events

• Adds schema migration 119 for auth_audit_log and a helper for recording PIN-login outcomes.

main/db.ts

settings.tsValidate and expose idle auto-lock setting +41/-0

Validate and expose idle auto-lock setting

• Defaults auto_lock_minutes to off, returns it to authenticated users, and limits writes to supported durations.

main/routes/settings.ts

package.jsonAdd PIN quick-switch regression command +2/-1

Add PIN quick-switch regression command

• Adds the targeted test script and includes it in the phase-two test sequence.

package.json

@qodo-code-review

qodo-code-review Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. PIN attempts can skip audit records ✓ Resolved
Description
pin-login returns HTTP 400 when user_id or pin is missing, before acquiring the database or
calling recordAuthAudit. Requests missing either field reach the sign-in endpoint but produce no
pin_login audit event, unlike other rejected PIN submissions.
Code

main/routes/auth.ts[R362-368]

+      const { user_id, pin, rememberMe } = req.body || {};
+      const userId = typeof user_id === 'string' ? user_id.trim() : '';
+      const rawPin = typeof pin === 'string' || typeof pin === 'number' ? String(pin) : '';
+
+      if (!userId || !rawPin) {
+        return res.status(400).json({ error: 'user_id and pin are required' });
+      }
Relevance

●●● Strong

The ticket explicitly requires PIN attempts to be audited, and recent observability findings were
accepted.

PR-#815
PR-#1247

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The missing-field validation branch returns before database acquisition and every call to
recordAuthAudit, so it cannot write an audit record. Later invalid-format and credential failures
are explicitly audited, and the documented PIN-login behavior requires every attempt to be recorded
with a failure or rate-limited outcome.

Rate-limit and audit PIN sign-ins
main/routes/auth.ts[362-368]
docs/architecture/authentication-and-authorization.md[203-208]
main/routes/auth.ts[360-402]
main/routes/auth.ts[425-457]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
PIN-login requests missing `user_id` or `pin` return before audit recording, leaving those sign-in attempts absent from the authentication audit trail.

## Fix Focus Areas
- main/routes/auth.ts[360-388]
- main/routes/auth.ts[370-397]

## Recommended Fix
Acquire the database and record a `pin_login` failure event before returning HTTP 400 for missing or malformed fields. Use a nullable or validated request-derived user ID and do not store the PIN. Add tests asserting an audit record for each missing-field case.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Parallel PIN guesses exceed the lockout ✓ Resolved
Description
pin-login checks the failed-attempt count before awaiting verifyPinAsync, then increments it
only after verification fails. Concurrent requests for one user can all pass the five-attempt check
before any completes, allowing extra guesses within the shared request-volume limit.
Code

main/routes/auth.ts[419]

+          pinMatches = await verifyPinAsync(user.pin_hash, rawPin);
Relevance

●●● Strong

Recent security reviews accept fixes for asynchronous concurrency races and recommend serialization
around sensitive operations.

PR-#1210
PR-#1247

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The limiter's check and increment are separate operations around an asynchronous bcrypt call. The
repository already has a per-key serialization helper for this precise race.

main/routes/auth.ts[370-426]
main/middleware/security.ts[139-166]
main/middleware/security.ts[225-246]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Concurrent PIN submissions can pass the failed-attempt check before asynchronous verification records a failure.
## Fix Focus Areas
- main/routes/auth.ts[370-450]
- main/middleware/security.ts[225-246]
## Recommended Fix
Use the existing per-key PIN verification lock around the failed-attempt check, verification, and counter update or reset. Add a concurrent-request regression test for the five-attempt boundary.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Some users share a PIN lockout ✓ Resolved
Description
POST /pin-login applies the shared IP-only loginAuthRateLimit() before its ${ip}:pin:${userId}
limiter; the shared limiter allows 10 requests per 15 minutes and counts successful sign-ins.
Because /login and the KDS logins use that limiter too, the 11th sign-in from a shared IP can
receive a 429 regardless of user or sign-in method, and rejected PIN attempts never reach per-user
handling or auditing.
Code

main/routes/auth.ts[R356-359]

+router.post(
+  '/pin-login',
+  loginAuthRateLimit(),
+  asyncHandler(async (req: Request, res: Response) => {
Relevance

●●● Strong

The issue intent explicitly requires per-user PIN limiting; recent authentication reviews accept
authorization and abuse-control corrections.

PR-#1247
PR-#779

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PIN route attaches loginAuthRateLimit() before the handler constructs its
${ip}:pin:${userId} key, so an outer rejection cannot reach checkLoginRateLimit or the audit
logic. The shared limiter is keyed by IP, has a limit of 10 requests per 15 minutes, and does not
set skipSuccessfulRequests; /login and the PIN route both attach it, and the KDS logins use it
too. The backend test raises FLO_AUTH_RATE_LIMIT_MAX to 100, hiding the default limit, while the
per-user failed-attempt lockout already provides PIN brute-force protection.

Rate-limit and audit PIN sign-ins
main/routes/auth.ts[356-359]
main/routes/auth.ts[370-384]
main/middleware/security.ts[85-112]
main/routes/auth.ts[243-250]
main/routes/auth.ts[356-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
PIN sign-in is gated by a shared IP-only limiter that counts successful requests and is also used by other login routes. On a shared device, the limit can reject another user's sign-in before PIN-specific rate limiting and auditing run.

## Fix Focus Areas
- main/routes/auth.ts[356-359]
- main/routes/auth.ts[370-384]
- main/middleware/security.ts[55-88]
- main/middleware/security.ts[85-112]

## Recommended Fix
Remove the shared IP-only limiter from the PIN route and rely on its per-user failed-attempt lockout (`checkLoginRateLimit`/`incrementFailedLogin`), or replace it with a PIN-specific limiter keyed by the selected user and normalized client address. If using a PIN-specific limiter, give it a ceiling appropriate for frequent user switches and set `skipSuccessfulRequests: true`. Preserve rate limiting and audit recording for PIN attempts, and add a test that performs more than 10 successful PIN logins from one IP without overriding `FLO_AUTH_RATE_LIMIT_MAX`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (1)
4. English approval errors say Manager PIN ✓ Resolved
Description
The English locale changes the kitchen-delivery prompt to Staff Approval PIN but leaves its paired
invalid-message string as Invalid Manager PIN. English users therefore receive inconsistent
approval terminology after a failed kitchen-delivery authorization.
Code

frontend/src/lib/i18n/messages/en.json[1130]

+    "kitchenDeliveryOverridePrompt": "Kitchen items are not yet delivered. Enter Staff Approval PIN to authorize billing.",
Relevance

●●● Strong

Recent locale reviews consistently accept user-facing terminology and translation corrections.

PR-#764
PR-#1043

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR changes the English kitchen-delivery prompt to the required approval terminology, but the
adjacent error message still uses Manager PIN, leaving a user-facing terminology violation in the
same locale.

Update PIN terminology and authorization documentation
frontend/src/lib/i18n/messages/en.json[1130-1132]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The English kitchen-delivery prompt now uses `Staff Approval PIN`, but the adjacent invalid-message string still tells users that the credential is a `Manager PIN`.

## Fix Focus Areas
- frontend/src/lib/i18n/messages/en.json[1130-1132]

## Recommended Fix
Change the English `kitchenDeliveryOverrideInvalid` message from `Invalid Manager PIN` to equivalent `Staff Approval PIN` wording, then verify all locale variants use consistent sign-in and approval terminology.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. A prior terminal timeout can lock users ✓ Resolved
Description
autoLockMinutes is added to the persisted POS settings without excluding it or clearing it when
the authenticated context changes. After a database or tenant switch, the dashboard can run an idle
timer using the previous setting while the new setting is loading, and a failed load leaves that
timer in place.
Code

frontend/src/store/pos-settings.ts[205]

+      autoLockMinutes: 0,
Relevance

●●● Strong

Recent settings and authentication reviews accept fixes preventing stale persisted context from
surviving tenant or session changes.

PR-#624
PR-#640

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Persistence includes the new timeout; hydration changes it only on a successful response, whereas
the timer uses the current store value and the loading path ignores failures.

frontend/src/store/pos-settings.ts[345-365]
frontend/src/app/(dashboard)/layout.tsx[26-68]
frontend/src/hooks/useReceiptSettingsSync.ts[103-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The persisted auto-lock timeout can be applied to a different authenticated database or tenant before its setting loads, or indefinitely when loading fails.
## Fix Focus Areas
- frontend/src/store/pos-settings.ts[345-365]
- frontend/src/app/(dashboard)/layout.tsx[20-68]
## Recommended Fix
Do not persist the backend-owned timeout as a global POS preference. Reset it on authentication-context changes and enable the timer only after the setting for the current context has been loaded successfully.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Locking the terminal leaves its session token valid ✓ Resolved
Description
lock() starts /auth/logout and synchronously removes the token that the Axios request
interceptor reads from localStorage to set the Authorization header. When the asynchronous
interceptor runs after removal, the request has no bearer token, so the server returns 200 without
revoking the user's JWT, including a potentially long-lived remember-me token.
Code

frontend/src/store/auth.ts[R139-142]

+    const logoutPromise = api.post('/auth/logout').catch(() => {});
+    try {
+      localStorage.removeItem('token');
+      localStorage.removeItem('tenant');
Relevance

●●● Strong

Recent authentication reviews accept fixes preventing stale asynchronous session operations from
affecting newer auth state.

PR-#624
PR-#1170

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The request interceptor reads the bearer token from localStorage when it runs, while lock()
removes that token immediately after starting the logout request. Because Axios request interceptors
run asynchronously by default, the interceptor can find no token; the backend calls revokeToken
only when the Authorization header is present and returns 200 otherwise, despite the PR docs saying
that lock revokes the active token.

frontend/src/lib/api.ts[106-115]
main/routes/auth.ts[546-559]
frontend/src/store/auth.ts[135-154]
frontend/src/lib/api.ts[99-108]
main/routes/auth.ts[541-555]
🌐 When you add request interceptors, they are presumed to be asynchronous by default ... a promise is created under the hood for the interceptor and your request gets put on the bottom of the call stack.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`lock()` and `logout()` start `/auth/logout` before synchronously removing the token. The asynchronous request interceptor can then find no token, leaving the JWT unrevoked on the server.

## Fix Focus Areas
- frontend/src/store/auth.ts[135-155]
- frontend/src/store/auth.ts[177-192]
- frontend/src/lib/api.ts[99-108]

## Recommended Fix
Capture the current token before clearing storage and pass it explicitly in the logout request's bearer authorization header, for example: `const token = useAuthStore.getState().token ?? localStorage.getItem('token'); api.post('/auth/logout', null, token ? { headers: { Authorization: `Bearer ${token}` } } : undefined)`. Keep clearing local authentication state immediately regardless of the request's outcome. Alternatively, register the request interceptor with `{ synchronous: true }`. Add a test asserting that the logout request carries the old bearer token.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. A failed save can undo a newer choice ✓ Resolved
Description
The auto-lock selector's request failure handler unconditionally restores the value captured before
that request. If an administrator selects two timeouts in quick succession and the first request
fails after the second succeeds, the selector and running timer revert to the old value despite the
newer value being saved.
Code

frontend/src/app/(dashboard)/settings/page.tsx[R4293-4296]

+                        void api
+                          .put('/settings/auto_lock_minutes', { value: String(next) })
+                          .catch(() => {
+                            posSettings.setAutoLockMinutes(prev);
Relevance

●●● Strong

Recent settings precedents accept stale asynchronous responses overwriting newer user changes.

PR-#1138
PR-#676

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Each change saves its own prev, and its catch handler writes that value without checking whether a
later selection or request has completed.

frontend/src/app/(dashboard)/settings/page.tsx[4284-4299]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An earlier failed auto-lock save can roll back a later successful selection.
## Fix Focus Areas
- frontend/src/app/(dashboard)/settings/page.tsx[4284-4299]
## Recommended Fix
Serialize setting writes or track a save generation and restore the previous value only when the failed request still corresponds to the latest selection. Test out-of-order completion of successive saves.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
8. Charge waivers can rewrite a bill paid mid-approval ✓ Resolved
Description
The PATCH /:id/charges handler is now async: it checks that the bill is unpaid and reads
bill/order, then awaits bcrypt verification in withPinVerificationLock. It then computes
charges and runs withTxn from those stale rows without re-checking payment_status,
paid_amount, order.status or discount_amount. A payment, cancellation or discount committed
during the bcrypt wait can be overwritten: order totals and charges_breakdown get rewritten on a
paid or cancelled order, or an older discount amount is written back.
Code

main/routes/bills.ts[5008]

+  asyncHandler(async (req: Request, res: Response) => {
Relevance

●● Moderate

A close prior stale-bill race was rejected, but this new bcrypt await creates a materially clearer
interleaving.

PR-#882
PR-#1210

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The validations at 5053-5103 use bill and order read before the new `await
withPinVerificationLock(...)` at 5231. The transaction at 5318+ only re-checks the approver, then
writes order.discount_amount and the charges computed from the stale bill.charges_breakdown.
Before this change the handler was synchronous, so nothing could run between the checks and the
write.

main/routes/bills.ts[5053-5103]
main/routes/bills.ts[5231-5285]
main/routes/bills.ts[5287-5380]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The charge-update handler now awaits PIN verification after validating `bill`/`order` but before writing. The transaction uses the stale rows without re-checking paid, refunded or cancelled state.

## Fix Focus Areas
- main/routes/bills.ts[5287-5380]

## Recommended Fix
Inside `withTxn`, re-select the bill and order. Re-apply the guards (split, paid/paid_amount, refunded, cancelled) and throw a 409 `statusCode` error if any fails. Compute `existingBreakdown`, toggles, resolved charges and the discount from the fresh rows, not the pre-await ones.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

9. Anyone on the network can read the auto-lock setting ✓ Resolved
Description
The new GET /api/settings/auto_lock_minutes route has only settingsReadRateLimit, with no
requirePermission/auth middleware, and the settings router has no router-level auth. Any
unauthenticated LAN client can read this setting, breaking the pattern that every other settings
read is permission-gated.
Code

main/routes/settings.ts[R1750-1755]

+router.get('/auto_lock_minutes', settingsReadRateLimit, (_req: Request, res: Response) => {
+  try {
+    const setting = getDatabase()
+      .prepare('SELECT * FROM settings WHERE key = ?')
+      .get('auto_lock_minutes');
+    if (setting) return res.json({ setting });
Relevance

●●● Strong

Recent settings reviews consistently accept missing permission gates and unauthorized access to
configuration endpoints.

PR-#1306
PR-#1247

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Neighbouring routes such as /business use requireBusinessSettingsRead or
requirePermission('settings.view'). The router is mounted at /api/settings with no auth wrapper.

main/routes/settings.ts[1750-1767]
main/routes/index.ts[106-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GET /api/settings/auto_lock_minutes` is reachable without authentication.

## Fix Focus Areas
- main/routes/settings.ts[1750-1767]

## Recommended Fix
Add an authenticate-only middleware (any signed-in role, since cashiers need the value for idle lock) before the handler, matching how other authenticated non-permissioned routes are guarded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
⚠️ Tickets: not configured — No jira, linear integration is connected for the linked tickets
✅ Compliance rules (platform): 5 rules
✅ Web pages:
  +2 more
✅ Cross-repo context — repo relationships
  Explored: repo: FreeOpenSourcePOS/FloAdmin (sha: e5d6830b) — View relationship
Review mode: Auto: 🧠 Deep: Broad auth changes span many independent security and session paths.

Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread main/routes/auth.ts Resolved
Comment thread main/routes/auth.ts Resolved
Comment thread frontend/src/lib/i18n/messages/en.json Resolved
Comment thread main/routes/auth.ts Resolved
Comment thread frontend/src/store/pos-settings.ts Resolved
Comment thread frontend/src/store/auth.ts Resolved
Comment thread main/routes/bills.ts Resolved
Comment thread main/routes/settings.ts Resolved
@khaira777 khaira777 changed the title feat(auth): add PIN quick user switch and terminal lock feat(auth): PIN quick user switch and picker Oct 28, 2026
@khaira777 khaira777 changed the title feat(auth): PIN quick user switch and picker feat(auth): add PIN quick user switch, terminal lock, and idle auto-lock Oct 28, 2026
@khaira777 khaira777 changed the title feat(auth): add PIN quick user switch, terminal lock, and idle auto-lock feat(auth): PIN quick user switch and picker Oct 28, 2026
Allow 4-6 digit PINs for all staff roles to support tile-based PIN quick user switching on the lock/login screen while keeping all approval paths strictly restricted to active owner/manager PINs.

Fixes #1338
- Use non-blocking per-request rate limit on POST /api/auth/pin-login while preserving per-user lockout
- Serialize per-user PIN verification with withPinVerificationLock and audit missing credential requests
- Re-read bill/order state and compute charge breakdowns inside the write transaction on PATCH /api/bills/:id/charges
- Guard GET /api/settings/auto_lock_minutes with requireAnyPermission across staff roles
- Pass explicit Bearer token to POST /auth/logout before clearing storage on lock/logout
- Exclude autoLockMinutes from localStorage persistence, reset on session changes, and guard stale save rollbacks
- Update kitchenDeliveryOverrideInvalid across all 24 locales to use Staff Approval PIN wording
…c.ts` line 270 to assert `page.getByText('Invalid Staff Approval PIN')` instead of `'Invalid manager PIN'`, matching the updated `pos.kitchenDeliveryOverrideInvalid` translation (`"Invalid Staff Approval PIN. Please try again."`) rendered by `PaymentModal.tsx` when `POST /api/bills/:id/payments` returns 403 `Invalid manager PIN`. The downstream `End-to-End Playwright Tests` and `Required Checks Gate` CI failures were caused by this Shard 1/4 E2E failure. Verification run and passed: - `npm run lint` - `npm run build && npm run build:frontend` - `npm run i18n:check` - `cd frontend && npx playwright test --config=playwright.config.ts --project=chromium e2e/approval-manager-selection.spec.ts e2e/pin-quick-switch.spec.ts` (4 passed) - `cd frontend && npx playwright test --config=playwright.config.ts --project=chromium e2e/kitchen-delivery-payment-retry.spec.ts e2e/kitchen/kitchen-delivery-billing-gate.spec.ts` (5 passed) - `node scripts/ci/check-changed-format.cjs --base 1278f91` - `git diff --check`
@khaira777
khaira777 force-pushed the fm/flocafe-1338-pin-quick-switch branch from 91478eb to 4abd1be Compare October 28, 2026 23:39
@khaira777 khaira777 changed the title feat(auth): PIN quick user switch and picker feat(auth): PIN quick user switch with user picker and terminal lock Oct 28, 2026
@khaira777 khaira777 changed the title feat(auth): PIN quick user switch with user picker and terminal lock feat(auth): fast cashier switch by PIN Oct 28, 2026
@khaira777
khaira777 merged commit 7196b82 into main Oct 28, 2026
23 checks passed
@khaira777
khaira777 deleted the fm/flocafe-1338-pin-quick-switch branch October 28, 2026 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(auth): PIN quick user switch with user picker (PIN for all roles, approvals stay owner/manager)

1 participant