feat(auth): proactive session expiry redirect across all delivery paths
- Split auth config into auth.config.ts (edge-safe, no argon2) and auth-edge.ts for middleware use; auth.ts now spreads the shared config - Middleware wraps with auth() to redirect unauthenticated requests to /auth/signin before any page render; passes through /auth/, /api/, /invite/ paths - SessionGuard client component watches useSession() and redirects on status=unauthenticated, closing the SPA navigation gap - QueryCache + MutationCache in TRPCProvider redirect on UNAUTHORIZED tRPC errors without retrying; SessionProvider polls session state every 5 minutes - Middleware tests updated for async auth wrapper and auth-edge mock Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
import { httpBatchLink, loggerLink } from "@trpc/client";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
@@ -12,6 +13,17 @@ function getBaseUrl() {
|
||||
return `http://localhost:${process.env["PORT"] ?? 3100}`;
|
||||
}
|
||||
|
||||
function redirectToSignIn(): void {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.replace("/auth/signin");
|
||||
}
|
||||
}
|
||||
|
||||
function isUnauthorizedTrpcError(error: unknown): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return error instanceof TRPCClientError && (error as any).data?.code === "UNAUTHORIZED";
|
||||
}
|
||||
|
||||
function isIgnorableTransportError(error: unknown): boolean {
|
||||
const message =
|
||||
typeof error === "object" && error !== null && "message" in error
|
||||
@@ -25,12 +37,26 @@ export function TRPCProvider({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: (error) => {
|
||||
if (isUnauthorizedTrpcError(error)) redirectToSignIn();
|
||||
},
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => {
|
||||
if (isUnauthorizedTrpcError(error)) redirectToSignIn();
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60_000, // 60 seconds — reduces refetches on navigation
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: 1,
|
||||
retry: (failureCount, error) => {
|
||||
// Never retry UNAUTHORIZED — redirect immediately instead
|
||||
if (isUnauthorizedTrpcError(error)) return false;
|
||||
return failureCount < 1;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -55,7 +81,7 @@ export function TRPCProvider({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<SessionProvider refetchInterval={5 * 60}>
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
|
||||
Reference in New Issue
Block a user