- 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>
86 lines
2.1 KiB
TypeScript
86 lines
2.1 KiB
TypeScript
import { useAuthStore } from '@/stores/auth.store'
|
|
import { usePOSStore } from '@/stores/pos.store'
|
|
|
|
class ApiError extends Error {
|
|
statusCode: number
|
|
details?: unknown
|
|
|
|
constructor(message: string, statusCode: number, details?: unknown) {
|
|
super(message)
|
|
this.name = 'ApiError'
|
|
this.statusCode = statusCode
|
|
this.details = details
|
|
}
|
|
}
|
|
|
|
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
// Use POS token if available (POS screen), otherwise admin token
|
|
const token = usePOSStore.getState().token ?? useAuthStore.getState().token
|
|
|
|
const headers: Record<string, string> = {}
|
|
|
|
if (body) {
|
|
headers['Content-Type'] = 'application/json'
|
|
}
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`
|
|
}
|
|
|
|
const res = await fetch(path, {
|
|
method,
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
})
|
|
|
|
if (res.status === 401) {
|
|
// On POS, lock the screen instead of logging out admin
|
|
if (usePOSStore.getState().token) {
|
|
usePOSStore.getState().lock()
|
|
} else {
|
|
useAuthStore.getState().logout()
|
|
}
|
|
throw new ApiError('Unauthorized', 401)
|
|
}
|
|
|
|
const json = await res.json()
|
|
|
|
if (!res.ok) {
|
|
throw new ApiError(
|
|
json.error?.message ?? 'Request failed',
|
|
res.status,
|
|
json.error?.details,
|
|
)
|
|
}
|
|
|
|
return json as T
|
|
}
|
|
|
|
function buildQueryString(params?: Record<string, unknown>): string {
|
|
if (!params) return ''
|
|
const searchParams = new URLSearchParams()
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
searchParams.set(key, String(value))
|
|
}
|
|
}
|
|
const qs = searchParams.toString()
|
|
return qs ? `?${qs}` : ''
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string, params?: Record<string, unknown>) =>
|
|
request<T>('GET', `${path}${buildQueryString(params)}`),
|
|
|
|
post: <T>(path: string, body: unknown) =>
|
|
request<T>('POST', path, body),
|
|
|
|
patch: <T>(path: string, body: unknown) =>
|
|
request<T>('PATCH', path, body),
|
|
|
|
del: <T>(path: string) =>
|
|
request<T>('DELETE', path),
|
|
}
|
|
|
|
export { ApiError }
|