diff --git a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts index 9ede84176..ced1c1ad4 100644 --- a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts @@ -20,6 +20,16 @@ function getAuthUrl(): string { return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; } +// Get backend URL dynamically at runtime +function getBackendUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) + .__PUBLIC_BACKEND_URL__; + return injectedUrl || 'http://localhost:3014'; + } + return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; +} + // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; @@ -27,7 +37,10 @@ let _tokenManager: ReturnType['tokenManager'] | null = function getAuthService() { if (!browser) return null; if (!_authService) { - const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); + const auth = initializeWebAuth({ + baseUrl: getAuthUrl(), + backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + }); _authService = auth.authService; _tokenManager = auth.tokenManager; } diff --git a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts index 92064596d..ed412923c 100644 --- a/apps/chat/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/chat/apps/web/src/lib/stores/auth.svelte.ts @@ -20,6 +20,16 @@ function getAuthUrl(): string { return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; } +// Get backend URL dynamically at runtime +function getBackendUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) + .__PUBLIC_BACKEND_URL__; + return injectedUrl || 'http://localhost:3002'; + } + return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3002'; +} + // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; @@ -27,7 +37,10 @@ let _tokenManager: ReturnType['tokenManager'] | null = function getAuthService() { if (!browser) return null; if (!_authService) { - const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); + const auth = initializeWebAuth({ + baseUrl: getAuthUrl(), + backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + }); _authService = auth.authService; _tokenManager = auth.tokenManager; } diff --git a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts index 2613ee0b5..931f6327e 100644 --- a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts @@ -19,6 +19,16 @@ function getAuthUrl(): string { return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; } +// Get backend URL dynamically at runtime +function getBackendUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) + .__PUBLIC_BACKEND_URL__; + return injectedUrl || 'http://localhost:3017'; + } + return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3017'; +} + // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; @@ -26,7 +36,10 @@ let _tokenManager: ReturnType['tokenManager'] | null = function getAuthService() { if (!browser) return null; if (!_authService) { - const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); + const auth = initializeWebAuth({ + baseUrl: getAuthUrl(), + backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + }); _authService = auth.authService; _tokenManager = auth.tokenManager; } diff --git a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts index 3122bf4f1..3f7320628 100644 --- a/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/contacts/apps/web/src/lib/stores/auth.svelte.ts @@ -8,8 +8,8 @@ import { initializeWebAuth } from '@manacore/shared-auth'; import type { UserData } from '@manacore/shared-auth'; // Initialize Mana Core Auth only on the client side -// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available const MANA_AUTH_URL = 'http://localhost:3001'; +const BACKEND_URL = 'http://localhost:3015'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; @@ -18,7 +18,10 @@ let _tokenManager: ReturnType['tokenManager'] | null = function getAuthService() { if (!browser) return null; if (!_authService) { - const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL }); + const auth = initializeWebAuth({ + baseUrl: MANA_AUTH_URL, + backendUrl: BACKEND_URL, // Enables automatic token refresh on 401 responses + }); _authService = auth.authService; _tokenManager = auth.tokenManager; } diff --git a/apps/picture/apps/web/src/lib/api/client.ts b/apps/picture/apps/web/src/lib/api/client.ts index 5e15d3754..3cac5de48 100644 --- a/apps/picture/apps/web/src/lib/api/client.ts +++ b/apps/picture/apps/web/src/lib/api/client.ts @@ -2,14 +2,16 @@ * API Client for Picture Backend * Replaces direct Supabase calls with backend API calls. * - * Token handling: Uses authStore.getValidToken() which automatically - * refreshes expired tokens before making requests. + * Token handling: + * - Uses authStore.getValidToken() which automatically refreshes expired tokens + * - The fetch interceptor (setupFetchInterceptor) handles 401 responses by refreshing and retrying + * - If refresh fails, the request fails and user should be redirected to login */ import { env } from '$env/dynamic/public'; import { authStore } from '$lib/stores/auth.svelte'; -const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3003'; +const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3006'; type FetchOptions = { method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'; diff --git a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts index b9ad5b753..b63abb804 100644 --- a/apps/picture/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/picture/apps/web/src/lib/stores/auth.svelte.ts @@ -7,6 +7,7 @@ import { browser } from '$app/environment'; import { env } from '$env/dynamic/public'; const MANA_AUTH_URL = env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; +const BACKEND_URL = env.PUBLIC_BACKEND_URL || 'http://localhost:3006'; export interface UserData { id: string; @@ -28,7 +29,10 @@ async function getAuthService() { if (!_authService) { try { const { initializeWebAuth } = await import('@manacore/shared-auth'); - const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL }); + const auth = initializeWebAuth({ + baseUrl: MANA_AUTH_URL, + backendUrl: BACKEND_URL, // Enables automatic token refresh on 401 responses + }); _authService = auth.authService; _tokenManager = auth.tokenManager; } catch (error) { diff --git a/apps/todo/apps/web/src/lib/stores/auth.svelte.ts b/apps/todo/apps/web/src/lib/stores/auth.svelte.ts index 7c3210601..817c251fd 100644 --- a/apps/todo/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/auth.svelte.ts @@ -20,6 +20,16 @@ function getAuthUrl(): string { return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; } +// Get backend URL dynamically at runtime +function getBackendUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) + .__PUBLIC_BACKEND_URL__; + return injectedUrl || 'http://localhost:3018'; + } + return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3018'; +} + // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; @@ -27,7 +37,10 @@ let _tokenManager: ReturnType['tokenManager'] | null = function getAuthService() { if (!browser) return null; if (!_authService) { - const auth = initializeWebAuth({ baseUrl: getAuthUrl() }); + const auth = initializeWebAuth({ + baseUrl: getAuthUrl(), + backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + }); _authService = auth.authService; _tokenManager = auth.tokenManager; } diff --git a/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts b/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts index 3122bf4f1..688e7e17f 100644 --- a/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/zitare/apps/web/src/lib/stores/auth.svelte.ts @@ -8,8 +8,8 @@ import { initializeWebAuth } from '@manacore/shared-auth'; import type { UserData } from '@manacore/shared-auth'; // Initialize Mana Core Auth only on the client side -// TODO: Use PUBLIC_MANA_CORE_AUTH_URL from env when available const MANA_AUTH_URL = 'http://localhost:3001'; +const BACKEND_URL = 'http://localhost:3007'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; @@ -18,7 +18,10 @@ let _tokenManager: ReturnType['tokenManager'] | null = function getAuthService() { if (!browser) return null; if (!_authService) { - const auth = initializeWebAuth({ baseUrl: MANA_AUTH_URL }); + const auth = initializeWebAuth({ + baseUrl: MANA_AUTH_URL, + backendUrl: BACKEND_URL, // Enables automatic token refresh on 401 responses + }); _authService = auth.authService; _tokenManager = auth.tokenManager; } diff --git a/packages/shared-auth/src/index.ts b/packages/shared-auth/src/index.ts index ce6391bb3..77db0d5f9 100644 --- a/packages/shared-auth/src/index.ts +++ b/packages/shared-auth/src/index.ts @@ -81,13 +81,21 @@ export type { ContactsClientConfig, ContactSearchOptions } from './clients/conta * ```typescript * import { initializeWebAuth } from '@manacore/shared-auth'; * + * // Basic setup (interceptor only for auth URL) * const { authService, tokenManager } = initializeWebAuth({ - * baseUrl: 'https://api.example.com', + * baseUrl: 'https://auth.example.com', + * }); + * + * // With backend URL (interceptor for both auth and backend - recommended) + * const { authService, tokenManager } = initializeWebAuth({ + * baseUrl: 'https://auth.example.com', + * backendUrl: 'https://api.example.com', * }); * ``` */ export function initializeWebAuth(config: { baseUrl: string; + backendUrl?: string; storageKeys?: Partial; }) { // Set up adapters @@ -99,8 +107,15 @@ export function initializeWebAuth(config: { const authService = _createAuthService(config); const tokenManager = _createTokenManager(authService); - // Set up interceptor + // Set up interceptor for auth URL _setupFetchInterceptor(authService, tokenManager); + // Set up interceptor for backend URL if provided (for automatic token refresh on 401) + if (config.backendUrl) { + _setupFetchInterceptor(authService, tokenManager, { + backendUrl: config.backendUrl, + }); + } + return { authService, tokenManager }; } diff --git a/packages/shared-auth/src/interceptors/fetchInterceptor.ts b/packages/shared-auth/src/interceptors/fetchInterceptor.ts index 259679352..78736519a 100644 --- a/packages/shared-auth/src/interceptors/fetchInterceptor.ts +++ b/packages/shared-auth/src/interceptors/fetchInterceptor.ts @@ -201,8 +201,9 @@ async function makeRequestWithToken( } /** - * Check if response indicates token expiration - * Only return true for explicit token expiration, not generic unauthorized errors + * Check if response indicates a token issue that warrants a refresh attempt + * Any 401 response should trigger a refresh attempt - if the refresh fails, + * then we know the session is truly invalid */ function isTokenExpiredResponse(responseData: Record): boolean { const error = responseData.error as Record | undefined; @@ -211,13 +212,21 @@ function isTokenExpiredResponse(responseData: Record): boolean ).toLowerCase(); const errorCode = String(responseData.code || error?.code || ''); - // Only trigger refresh for explicit token expiration messages + // Trigger refresh for any token-related auth error + // This includes: + // - Explicit expiration: "jwt expired", "token expired" + // - Generic validation failures: "invalid token", "token validation failed" + // - Backend passthrough errors: "exp claim", "claim timestamp" return ( errorMessage.includes('jwt expired') || errorMessage.includes('token expired') || errorMessage.includes('token has expired') || + errorMessage.includes('invalid token') || + errorMessage.includes('token validation failed') || + errorMessage.includes('claim') || // Catches jose errors like "exp claim timestamp check failed" errorCode === 'PGRST301' || - errorCode === 'TOKEN_EXPIRED' + errorCode === 'TOKEN_EXPIRED' || + errorCode === 'ERR_JWT_EXPIRED' ); }