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:
2026-04-03 10:42:10 +02:00
parent ed4d4e4640
commit bf8577dbaf
8 changed files with 151 additions and 57 deletions
+29 -3
View File
@@ -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>