mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 10:01:08 +02:00
Projects included: - maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing) - manacore (Expo mobile + SvelteKit web + Astro landing) - manadeck (NestJS backend + Expo mobile + SvelteKit web) - memoro (Expo mobile + SvelteKit web + Astro landing) This commit preserves the current state before monorepo restructuring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
156 lines
No EOL
4.9 KiB
TypeScript
156 lines
No EOL
4.9 KiB
TypeScript
import { tokenManager, TokenState } from '../services/tokenManager';
|
|
import { updateSupabaseAuth } from '../lib/supabaseClient';
|
|
import { authService } from '../services/authService';
|
|
|
|
/**
|
|
* Set up a global fetch interceptor to handle token refresh for all API calls
|
|
* Uses the enhanced TokenManager for race condition-free token management
|
|
*/
|
|
export const setupGlobalFetchInterceptor = (): void => {
|
|
if (typeof globalThis !== 'undefined' && globalThis.fetch) {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = (async (input, init) => {
|
|
const url = extractUrl(input);
|
|
|
|
// Skip intercepting for non-API calls or calls that don't need auth
|
|
if (shouldSkipInterception(url)) {
|
|
return originalFetch(input, init);
|
|
}
|
|
|
|
try {
|
|
// First, try the request with current token
|
|
const response = await makeRequestWithToken(originalFetch, input, init);
|
|
|
|
// If we get a 401, let TokenManager handle it
|
|
if (response.status === 401) {
|
|
const responseData = await response.clone().json().catch(() => ({}));
|
|
console.debug('Received 401 response:', responseData);
|
|
|
|
const isTokenExpired = isTokenExpiredResponse(responseData);
|
|
|
|
if (isTokenExpired) {
|
|
console.debug('Token expired, delegating to TokenManager');
|
|
// Use TokenManager to handle the 401 and retry
|
|
return await tokenManager.handle401Response(input, init);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.debug('Error in global fetch interceptor:', error);
|
|
return originalFetch(input, init);
|
|
}
|
|
}) as typeof fetch;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Make a request with the current token
|
|
*/
|
|
async function makeRequestWithToken(
|
|
originalFetch: typeof fetch,
|
|
input: RequestInfo | URL,
|
|
init?: RequestInit
|
|
): Promise<Response> {
|
|
// Get current token (don't force refresh here)
|
|
const token = await authService.getAppToken();
|
|
|
|
if (token) {
|
|
const headers = new Headers(init?.headers || {});
|
|
headers.set('Authorization', `Bearer ${token}`);
|
|
init = { ...init, headers };
|
|
}
|
|
|
|
return originalFetch(input, init);
|
|
}
|
|
|
|
/**
|
|
* Reset the refresh attempts counter (call this after successful operations)
|
|
*/
|
|
export const resetRefreshAttempts = (): void => {
|
|
// TokenManager handles this internally, but we can provide a way to reset state
|
|
tokenManager.reset();
|
|
};
|
|
|
|
/**
|
|
* Handle authentication failure by clearing storage and redirecting to login
|
|
*/
|
|
async function handleAuthFailure(): Promise<void> {
|
|
try {
|
|
await authService.clearAuthStorage();
|
|
|
|
// Use setTimeout to avoid React state updates during render
|
|
setTimeout(() => {
|
|
try {
|
|
const { router } = require('expo-router');
|
|
router.replace('/(public)/login');
|
|
} catch (routerError) {
|
|
console.debug('Error redirecting to login:', routerError);
|
|
}
|
|
}, 100);
|
|
} catch (error) {
|
|
console.debug('Error handling auth failure:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract URL from various input types
|
|
*/
|
|
function extractUrl(input: RequestInfo | URL): string {
|
|
if (typeof input === 'string') {
|
|
return input;
|
|
} else if (input instanceof URL) {
|
|
return input.toString();
|
|
} else if (input instanceof Request) {
|
|
return input.url;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Check if request should skip interception
|
|
*/
|
|
function shouldSkipInterception(url: string): boolean {
|
|
return !url ||
|
|
url.includes('auth/signin') ||
|
|
url.includes('auth/signup') ||
|
|
url.includes('auth/refresh') ||
|
|
url.includes('auth/forgot-password') ||
|
|
// Skip Supabase storage operations to avoid ES256 JWT issues
|
|
url.includes('.supabase.co/storage/') ||
|
|
url.includes('/storage/v1/');
|
|
}
|
|
|
|
/**
|
|
* Check if response indicates token expiration
|
|
*/
|
|
function isTokenExpiredResponse(responseData: any): boolean {
|
|
return responseData.error?.message === 'JWT expired' ||
|
|
responseData.message === 'JWT expired' ||
|
|
responseData.error === 'JWT expired' ||
|
|
responseData.code === 'PGRST301' ||
|
|
responseData.error?.code === 'PGRST301' ||
|
|
responseData.message === 'Unauthorized' ||
|
|
responseData.error === 'Unauthorized';
|
|
}
|
|
|
|
/**
|
|
* Setup token manager observers for Supabase integration
|
|
*/
|
|
export const setupTokenObservers = (): void => {
|
|
// Subscribe to token state changes to update Supabase
|
|
tokenManager.subscribe(async (state: TokenState, token?: string) => {
|
|
try {
|
|
if (state === TokenState.VALID && token) {
|
|
console.debug('TokenManager: Token valid, updating Supabase auth');
|
|
await updateSupabaseAuth();
|
|
} else if (state === TokenState.EXPIRED) {
|
|
console.debug('TokenManager: Token expired, clearing Supabase auth');
|
|
// Supabase will handle the expired token automatically
|
|
}
|
|
} catch (error) {
|
|
console.debug('Error updating Supabase auth from token observer:', error);
|
|
}
|
|
});
|
|
}; |