Files
Vector/apps/web/src/contexts/AuthContext.tsx
T
josh 7c0d422228
CI / Lint · Typecheck · Test · Build (push) Failing after 5m41s
CI / Playwright (smoke) (push) Has been skipped
chore: initial Vector 2.0 monorepo
Ground-up TypeScript rewrite of the Vector hardware parts inventory
system. Ships the full roadmap (Phases 0-8) in one initial commit:

- pnpm + Turbo monorepo: apps/{api,web,e2e}, packages/{db,shared,ui,config}
- Express 5 + Prisma 5 + zod validation + JWT w/ refresh-token rotation
- React 19 + Vite + shadcn/ui + TanStack Query/Table + nuqs URL state
- Repair/RMA, tags, bulk ops, saved views, CSV audit export
- Analytics dashboard on Recharts + EOL tracking
- Signed webhook subscriptions (HMAC-SHA256) with in-process emitter
- Vitest unit tests (shared schemas, api services/helpers) + Playwright skeleton
- Gitea Actions CI (lint, typecheck, test+coverage, build) + Renovate

Deferred follow-ups: Postgres cutover (data-migration script ready),
BullMQ worker for webhook delivery, @react-pdf PDF export, CSV import wizard.
2026-04-16 20:52:32 -04:00

74 lines
2.2 KiB
TypeScript

import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { login as apiLogin, logout as apiLogout, me } from '../lib/api/auth.js';
import type { AuthUser } from '../lib/api/auth.js';
import { ApiRequestError } from '../lib/api/client.js';
interface AuthContextValue {
user: AuthUser | null;
status: 'loading' | 'authenticated' | 'anonymous';
login: (username: string, password: string) => Promise<AuthUser>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [status, setStatus] = useState<AuthContextValue['status']>('loading');
const qc = useQueryClient();
// Bootstrap: try /me once. If the refresh interceptor revives a dead access token, this
// round-trips once; on a hard 401 the server returns anonymous and we show the Login page.
useEffect(() => {
let cancelled = false;
(async () => {
try {
const u = await me();
if (!cancelled) {
setUser(u);
setStatus('authenticated');
}
} catch (err) {
if (cancelled) return;
if (err instanceof ApiRequestError && err.status === 401) {
setStatus('anonymous');
} else {
setStatus('anonymous');
}
}
})();
return () => {
cancelled = true;
};
}, []);
const login: AuthContextValue['login'] = async (username, password) => {
const u = await apiLogin(username, password);
setUser(u);
setStatus('authenticated');
return u;
};
const logout: AuthContextValue['logout'] = async () => {
try {
await apiLogout();
} catch {
// Ignore — tokens are best-effort-revoked server side.
}
setUser(null);
setStatus('anonymous');
qc.clear();
};
return (
<AuthContext.Provider value={{ user, status, login, logout }}>{children}</AuthContext.Provider>
);
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within <AuthProvider>');
return ctx;
}