mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 18:33:01 +00:00
SessionProvider: centralised state management
this replaces auth-provider, centralising user data can be extended to keep additional data allows for user data to propogate components throughout the app provides useSession and useAuthenticatedSession()
This commit is contained in:
84
packages/frontend/src/components/session-provider.tsx
Normal file
84
packages/frontend/src/components/session-provider.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { UserRecord } from "@issue/shared";
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import Loading from "@/components/loading";
|
||||
import { clearAuth, getServerURL, setCsrfToken } from "@/lib/utils";
|
||||
|
||||
interface SessionContextValue {
|
||||
user: UserRecord | null;
|
||||
setUser: (user: UserRecord) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextValue | null>(null);
|
||||
|
||||
// for use outside RequireAuth
|
||||
export function useSession(): SessionContextValue {
|
||||
const context = useContext(SessionContext);
|
||||
if (!context) {
|
||||
throw new Error("useSession must be used within a SessionProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// for use inside RequireAuth
|
||||
export function useAuthenticatedSession(): { user: UserRecord; setUser: (user: UserRecord) => void } {
|
||||
const { user, setUser } = useSession();
|
||||
if (!user) {
|
||||
throw new Error("useAuthenticatedSession must be used within RequireAuth");
|
||||
}
|
||||
return { user, setUser };
|
||||
}
|
||||
|
||||
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUserState] = useState<UserRecord | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const fetched = useRef(false);
|
||||
|
||||
const setUser = useCallback((user: UserRecord) => {
|
||||
setUserState(user);
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetched.current) return;
|
||||
fetched.current = true;
|
||||
|
||||
fetch(`${getServerURL()}/auth/me`, {
|
||||
credentials: "include",
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`auth check failed: ${res.status}`);
|
||||
}
|
||||
const data = (await res.json()) as { user: UserRecord; csrfToken: string };
|
||||
setUser(data.user);
|
||||
setCsrfToken(data.csrfToken);
|
||||
})
|
||||
.catch(() => {
|
||||
setUserState(null);
|
||||
clearAuth();
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [setUser]);
|
||||
|
||||
return <SessionContext.Provider value={{ user, setUser, isLoading }}>{children}</SessionContext.Provider>;
|
||||
}
|
||||
|
||||
export function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||
const { user, isLoading } = useSession();
|
||||
const location = useLocation();
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading message={"Checking authentication"} />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
const next = encodeURIComponent(location.pathname + location.search);
|
||||
return <Navigate to={`/login?next=${next}`} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user