furniture-web/src/auth/sessionContext.tsx
2026-03-08 16:23:00 -06:00

121 lines
3.2 KiB
TypeScript

import { createContext, type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { ApiError } from '../api/client';
import {
getSessionMe,
loginSession,
logoutSession,
refreshSession,
type SessionLoginRequest,
type SessionProfile
} from '../api/sessionApi';
export type SessionStatus = 'loading' | 'authenticated' | 'unauthenticated';
type SessionContextValue = {
status: SessionStatus;
profile: SessionProfile | null;
error: string | null;
login: (request: Omit<SessionLoginRequest, 'correlationId'> & { correlationId?: string }) => Promise<void>;
refresh: () => Promise<void>;
logout: () => Promise<void>;
revalidate: () => Promise<void>;
};
const SessionContext = createContext<SessionContextValue | undefined>(undefined);
export function SessionProvider({ children }: PropsWithChildren) {
const [status, setStatus] = useState<SessionStatus>('loading');
const [profile, setProfile] = useState<SessionProfile | null>(null);
const [error, setError] = useState<string | null>(null);
const revalidate = useCallback(async () => {
try {
const me = await getSessionMe();
if (me.isAuthenticated) {
setProfile(me);
setStatus('authenticated');
} else {
setProfile(null);
setStatus('unauthenticated');
}
setError(null);
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
setProfile(null);
setStatus('unauthenticated');
setError(null);
return;
}
const message = err instanceof Error ? err.message : 'Session validation failed.';
setProfile(null);
setStatus('unauthenticated');
setError(message);
}
}, []);
useEffect(() => {
void revalidate();
}, [revalidate]);
const login = useCallback<SessionContextValue['login']>(
async (request) => {
setError(null);
await loginSession({
...request,
correlationId: request.correlationId && request.correlationId.length > 0 ? request.correlationId : createCorrelationId()
});
await revalidate();
},
[revalidate]
);
const refresh = useCallback(async () => {
setError(null);
await refreshSession();
await revalidate();
}, [revalidate]);
const logout = useCallback(async () => {
setError(null);
try {
await logoutSession();
} finally {
setProfile(null);
setStatus('unauthenticated');
}
}, []);
const value = useMemo<SessionContextValue>(
() => ({
status,
profile,
error,
login,
refresh,
logout,
revalidate
}),
[status, profile, error, login, refresh, logout, revalidate]
);
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
}
export function useSessionContext(): SessionContextValue {
const value = useContext(SessionContext);
if (!value) {
throw new Error('useSessionContext must be used within SessionProvider.');
}
return value;
}
function createCorrelationId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return `corr-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}