/** * Shared auth gate so axios 401 handler does not fight Clerk session state. * ApiInterceptor writes; client interceptor reads. */ let clerkLoaded = false; let clerkSignedIn = false; let hardRedirectArmed = false; let lastRedirectAt = 0; export function setClerkAuthState(opts: { isLoaded: boolean; isSignedIn: boolean }) { clerkLoaded = opts.isLoaded; clerkSignedIn = Boolean(opts.isSignedIn); // Allow a hard redirect only after Clerk finished loading and user is truly out hardRedirectArmed = clerkLoaded && !clerkSignedIn; } export function getClerkAuthState() { return { clerkLoaded, clerkSignedIn, hardRedirectArmed }; } /** * Should a 401 response force navigation to /sign-in? * - Never while Clerk still loading * - Never while Clerk says the user IS signed in (token race / backend lag) * - Throttle hard redirects to avoid bounce loops */ export function shouldHardRedirectOn401(): boolean { if (!clerkLoaded) return false; if (clerkSignedIn) return false; if (!hardRedirectArmed) return false; const now = Date.now(); if (now - lastRedirectAt < 2500) return false; lastRedirectAt = now; return true; } export function safeSignInRedirectPath(currentPath: string, search = ''): string { const path = currentPath || '/projects'; if ( path === '/' || path.startsWith('/sign-in') || path.startsWith('/sign-up') || path.startsWith('/pricing') || path.startsWith('/cennik') || path.startsWith('/regulamin') || path.startsWith('/polityka') ) { return '/sign-in'; } const returnTo = encodeURIComponent(path + (search || '')); return `/sign-in?redirect_url=${returnTo}`; } /** Resolve post-login landing: query redirect_url → fallback /projects (not /dashboard). */ export function resolvePostAuthRedirect(search: string): string { try { const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search); const raw = params.get('redirect_url') || params.get('redirectUrl') || ''; if (raw) { const decoded = decodeURIComponent(raw); // Only same-origin relative paths if (decoded.startsWith('/') && !decoded.startsWith('//') && !decoded.includes('://')) { // Avoid bounce: /dashboard is an alias of /projects if (decoded === '/dashboard' || decoded.startsWith('/dashboard?')) { return '/projects' + (decoded.includes('?') ? decoded.slice(decoded.indexOf('?')) : ''); } return decoded; } } } catch { /* ignore */ } return '/projects'; }