Files
lunarfront-app/packages/admin/src/lib/api-client.ts
ryan cf299ac1d2 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>
2026-04-05 16:05:19 +00:00

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 }