CampgroundTickets/app/lib/auth.tsx
Hank 0e8fe3bb9a 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>
2026-07-08 16:45:23 +00:00

64 lines
1.8 KiB
TypeScript

import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import {
login as apiLogin,
logout as apiLogout,
getToken,
getOperator,
setOperator as apiSetOperator,
onAuthCleared,
} from "./api";
interface AuthState {
ready: boolean; // finished the initial load
signedIn: boolean; // has a valid PIN token
operator: string; // gate staff name (empty until set)
signIn: (pin: string) => Promise<void>;
setOperator: (name: string) => Promise<void>;
signOut: () => Promise<void>;
}
const Ctx = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [ready, setReady] = useState(false);
const [signedIn, setSignedIn] = useState(false);
const [operator, setOperatorState] = useState("");
useEffect(() => {
Promise.all([getToken(), getOperator()]).then(([t, op]) => {
setSignedIn(!!t);
setOperatorState(op ?? "");
setReady(true);
});
// Keep state in sync when the token is cleared elsewhere (401 handling).
onAuthCleared(() => {
setSignedIn(false);
setOperatorState("");
});
return () => onAuthCleared(null);
}, []);
const signIn = async (pin: string) => {
await apiLogin(pin);
setSignedIn(true);
};
const setOperator = async (name: string) => {
await apiSetOperator(name);
setOperatorState(name);
};
const signOut = async () => {
await apiLogout();
setSignedIn(false);
setOperatorState("");
};
return (
<Ctx.Provider value={{ ready, signedIn, operator, signIn, setOperator, signOut }}>{children}</Ctx.Provider>
);
}
export function useAuth(): AuthState {
const c = useContext(Ctx);
if (!c) throw new Error("useAuth must be used within AuthProvider");
return c;
}