7c0d422228
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.
74 lines
2.2 KiB
TypeScript
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;
|
|
}
|