feat: POS PIN unlock with employee number + PIN auth

- Add employeeNumber and pinHash fields to users table
- POST /auth/pin-login: takes combined code (4-digit employee# + 4-digit PIN)
- POST /auth/set-pin: employee sets their own PIN (requires full auth)
- DELETE /auth/pin: remove PIN
- Lock screen with numpad, auto-submits on 8 digits, visual dot separator
- POS uses its own auth token separate from admin session
- Admin "POS" link clears admin session before navigating
- /pos route has no auth guard — lock screen is the auth
- API client uses POS token when available, admin token otherwise
- Auto-lock timer reads pos_lock_timeout from app_config (default 15 min)
- Lock button in POS top bar, shows current cashier name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-04 20:59:09 +00:00
parent 978c6da37a
commit 24b2b8c292
13 changed files with 396 additions and 39 deletions

View File

@@ -1,12 +1,27 @@
import { create } from 'zustand'
interface POSUser {
id: string
email: string
firstName: string
lastName: string
role: string
}
interface POSState {
currentTransactionId: string | null
locationId: string | null
drawerSessionId: string | null
locked: boolean
cashier: POSUser | null
token: string | null
lastActivity: number
setTransaction: (id: string | null) => void
setLocation: (id: string) => void
setDrawerSession: (id: string | null) => void
unlock: (user: POSUser, token: string) => void
lock: () => void
touchActivity: () => void
reset: () => void
}
@@ -14,8 +29,15 @@ export const usePOSStore = create<POSState>((set) => ({
currentTransactionId: null,
locationId: null,
drawerSessionId: null,
locked: true,
cashier: null,
token: null,
lastActivity: Date.now(),
setTransaction: (id) => set({ currentTransactionId: id }),
setLocation: (id) => set({ locationId: id }),
setDrawerSession: (id) => set({ drawerSessionId: id }),
unlock: (user, token) => set({ locked: false, cashier: user, token, lastActivity: Date.now() }),
lock: () => set({ locked: true, currentTransactionId: null }),
touchActivity: () => set({ lastActivity: Date.now() }),
reset: () => set({ currentTransactionId: null }),
}))