Backend: add refresh JWT (30d, token_type claim), POST /auth/refresh endpoint (rotates token pair), remember_me on login, JWT_REFRESH_EXPIRY_DAYS env var. Extractors now reject refresh tokens on protected routes. Frontend: sessionStorage for non-remembered sessions, localStorage + refresh token for remembered sessions. Transparent 401 recovery in api.ts (retry once after refresh). Remember me checkbox on login page with security note when checked.
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
QueryCache,
|
|
QueryClient,
|
|
QueryClientProvider,
|
|
MutationCache,
|
|
} from "@tanstack/react-query";
|
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
import { useState, useRef, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import { AuthProvider, useAuthContext } from "@/context/auth-context";
|
|
import { Toaster } from "@/components/ui/sonner";
|
|
import { ApiRequestError } from "@/lib/api";
|
|
|
|
function QueryProvider({ children }: { children: React.ReactNode }) {
|
|
const { token, setTokens } = useAuthContext();
|
|
const router = useRouter();
|
|
const tokenRef = useRef(token);
|
|
useEffect(() => { tokenRef.current = token; }, [token]);
|
|
|
|
/* eslint-disable react-hooks/refs -- tokenRef is only read in onError callbacks, not during render */
|
|
const [queryClient] = useState(() => {
|
|
return new QueryClient({
|
|
queryCache: new QueryCache({
|
|
onError: (error) => {
|
|
// Only redirect on 401 if the user had a token (expired session).
|
|
// Guests hitting 401 on restricted content should not be redirected.
|
|
if (error instanceof ApiRequestError && error.status === 401 && tokenRef.current) {
|
|
toast.warning("Session expired, please log in again.");
|
|
setTokens(null, null, false);
|
|
router.push("/login");
|
|
}
|
|
},
|
|
}),
|
|
mutationCache: new MutationCache({
|
|
onError: (error) => {
|
|
// Mutations always require auth — redirect on 401 regardless.
|
|
if (error instanceof ApiRequestError && error.status === 401) {
|
|
toast.warning("Session expired, please log in again.");
|
|
setTokens(null, null, false);
|
|
router.push("/login");
|
|
}
|
|
},
|
|
}),
|
|
defaultOptions: { queries: { staleTime: 60 * 1000 } },
|
|
});
|
|
});
|
|
/* eslint-enable react-hooks/refs */
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
{children}
|
|
<Toaster position="bottom-right" richColors />
|
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<AuthProvider>
|
|
<QueryProvider>{children}</QueryProvider>
|
|
</AuthProvider>
|
|
);
|
|
}
|