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.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user