4-digit auto-submit PIN + operator name in audit logs

Login: fixed-length 4-digit PIN that auto-submits on the 4th digit (no submit
button to scroll to on small iPhone screens) and clears on a wrong PIN.
Compact, vertically-centered keypad so it fits without scrolling.

Operator tracking: after PIN auth, staff enter their name (new /operator
screen, persisted per device). The name is sent as X-Operator on every authed
request and recorded on each check-in/undo/ice audit entry (new Operator
column), so logs show who did what. Shown in the scanner header and the admin
audit view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-08 16:45:23 +00:00
parent 336d2a5c83
commit 0e8fe3bb9a
11 changed files with 311 additions and 102 deletions

View file

@ -2,6 +2,7 @@ import { Platform } from "react-native";
// Token persistence: localStorage on web, SecureStore on native.
const KEY = "campscan.token";
const OPERATOR_KEY = "campscan.operator";
export async function saveToken(token: string): Promise<void> {
if (Platform.OS === "web") {
@ -40,3 +41,44 @@ export async function clearToken(): Promise<void> {
const SecureStore = await import("expo-secure-store");
await SecureStore.deleteItemAsync(KEY);
}
// Operator (gate staff) name — plain persistence, not sensitive. Kept in
// localStorage on both platforms for simplicity (SecureStore is overkill here;
// on native we still use localStorage-less AsyncStorage-free approach below).
export async function saveOperator(name: string): Promise<void> {
if (Platform.OS === "web") {
try {
window.localStorage.setItem(OPERATOR_KEY, name);
} catch {
/* ignore */
}
return;
}
const SecureStore = await import("expo-secure-store");
await SecureStore.setItemAsync(OPERATOR_KEY, name);
}
export async function loadOperator(): Promise<string | null> {
if (Platform.OS === "web") {
try {
return window.localStorage.getItem(OPERATOR_KEY);
} catch {
return null;
}
}
const SecureStore = await import("expo-secure-store");
return SecureStore.getItemAsync(OPERATOR_KEY);
}
export async function clearOperator(): Promise<void> {
if (Platform.OS === "web") {
try {
window.localStorage.removeItem(OPERATOR_KEY);
} catch {
/* ignore */
}
return;
}
const SecureStore = await import("expo-secure-store");
await SecureStore.deleteItemAsync(OPERATOR_KEY);
}