121 lines
3.2 KiB
TypeScript
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)}`;
|
|
}
|