From 0717a72a83a343b78fb8f5af5161ee4fb7061159 Mon Sep 17 00:00:00 2001 From: arolleaguekeng Date: Thu, 9 Jul 2026 01:07:31 +0100 Subject: [PATCH] refactor: optimize auth interceptor flow, implement project cache invalidation, and increase API request timeouts for branding operations. --- apps/api/api/config/ai.config.ts | 4 +- apps/api/api/config/cors.config.ts | 2 + apps/api/api/index.ts | 14 ++- apps/api/api/routes/branding.routes.ts | 11 ++ apps/api/api/services/project.service.ts | 13 ++ apps/ideploy/package-lock.json | 22 ++-- .../complete-branding/complete-branding.ts | 55 +++++++++ .../logo-selection/logo-selection.ts | 16 +++ .../logo-variations/logo-variations.ts | 48 +++++++- .../pages/create-project/create-project.ts | 1 + .../services/ai-agents/branding.service.ts | 8 +- .../shared/interceptors/auth.interceptor.ts | 116 ++++++++++-------- 12 files changed, 239 insertions(+), 71 deletions(-) diff --git a/apps/api/api/config/ai.config.ts b/apps/api/api/config/ai.config.ts index 523bfac15..0bd770991 100644 --- a/apps/api/api/config/ai.config.ts +++ b/apps/api/api/config/ai.config.ts @@ -173,9 +173,9 @@ export const AI_CONFIG = { branding: { logo: { provider: LLMProvider.GEMINI, - modelName: 'gemini-2.5-flash', + modelName: 'gemini-3-flash-preview', llmOptions: { - maxOutputTokens: 1600, + maxOutputTokens: 4000, temperature: 0.5, topP: 0.95, topK: 40, diff --git a/apps/api/api/config/cors.config.ts b/apps/api/api/config/cors.config.ts index 8a071237f..b07f16556 100644 --- a/apps/api/api/config/cors.config.ts +++ b/apps/api/api/config/cors.config.ts @@ -52,5 +52,7 @@ export function buildCorsOptions(): CorsOptions { ], exposedHeaders: ['Content-Type', 'Cache-Control', 'Connection', 'X-Accel-Buffering'], maxAge: 600, + optionsSuccessStatus: 200, }; } + diff --git a/apps/api/api/index.ts b/apps/api/api/index.ts index 4e9e34a13..ae90b8bb4 100644 --- a/apps/api/api/index.ts +++ b/apps/api/api/index.ts @@ -184,9 +184,21 @@ app.use((req: Request, res: Response) => { app.use((err: Error, req: Request, res: Response /*, next: NextFunction */) => { console.error('Global error handler:', err); - res.status(500).send('Something broke!'); + + // S'assurer que les en-têtes CORS sont présents même en cas d'erreur + const origin = req.headers.origin; + if (origin) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + } + + res.status(500).json({ + error: 'Internal Server Error', + message: err.message || 'Something broke!' + }); }); + async function bootstrap() { await loadSecrets(); initFirebase(); diff --git a/apps/api/api/routes/branding.routes.ts b/apps/api/api/routes/branding.routes.ts index 61ec3cd95..fb00bd9fb 100644 --- a/apps/api/api/routes/branding.routes.ts +++ b/apps/api/api/routes/branding.routes.ts @@ -20,6 +20,14 @@ export const brandingRoutes = Router(); const resourceName = 'brandings'; +// Middleware to extend connection timeout for heavy processing tasks (AI generation, PDF, etc.) +const extendedTimeout = (req: any, res: any, next: any) => { + req.setTimeout(180000); // 3 minutes + res.setTimeout(180000); // 3 minutes + next(); +}; + + // All routes are protected and project-specific where applicable // Generate a new branding for a project @@ -207,6 +215,7 @@ brandingRoutes.post( brandingRoutes.post( `/${resourceName}/generate/logo-concepts/:projectId`, authenticate, + extendedTimeout, checkQuota, generateLogoConceptsController ); @@ -263,6 +272,7 @@ brandingRoutes.post( brandingRoutes.post( `/${resourceName}/generate/logo-variations/:projectId`, authenticate, + extendedTimeout, checkQuota, generateLogoVariationsController ); @@ -700,6 +710,7 @@ brandingRoutes.get( brandingRoutes.post( `/${resourceName}/edit-logo/:projectId`, authenticate, + extendedTimeout, checkQuota, editLogoController ); diff --git a/apps/api/api/services/project.service.ts b/apps/api/api/services/project.service.ts index 4a0d770e3..c77f5be4d 100644 --- a/apps/api/api/services/project.service.ts +++ b/apps/api/api/services/project.service.ts @@ -7,6 +7,7 @@ import fsExtra from 'fs-extra'; import path from 'path'; import { storageService } from './storage.service'; import { v4 as uuidv4 } from 'uuid'; +import { cacheService } from './cache.service'; class ProjectService { private projectRepository: IRepository; @@ -206,6 +207,12 @@ class ProjectService { const success = await this.projectRepository.delete(projectId, `users/${userId}/projects`); if (success) { logger.info(`Project ${projectId} deleted successfully for user ${userId} via repository`); + + // Invalider le cache du projet dans Redis + const projectCacheKey = `project_${userId}_${projectId}`; + await cacheService.delete(projectCacheKey, { prefix: 'project' }).catch((err) => { + logger.error(`Error invalidating project cache for key ${projectCacheKey}:`, err); + }); } else { logger.warn( `Project ${projectId} not found for deletion or delete failed for user ${userId} via repository` @@ -238,6 +245,12 @@ class ProjectService { ); if (updatedProject) { logger.info(`Project ${projectId} updated successfully for user ${userId} via repository`); + + // Invalider le cache du projet dans Redis + const projectCacheKey = `project_${userId}_${projectId}`; + await cacheService.delete(projectCacheKey, { prefix: 'project' }).catch((err) => { + logger.error(`Error invalidating project cache for key ${projectCacheKey}:`, err); + }); } else { logger.warn( `Project ${projectId} not found for update or update failed for user ${userId} via repository` diff --git a/apps/ideploy/package-lock.json b/apps/ideploy/package-lock.json index bdddd052d..6c2930d89 100644 --- a/apps/ideploy/package-lock.json +++ b/apps/ideploy/package-lock.json @@ -938,7 +938,8 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@tailwindcss/forms": { "version": "0.5.10", @@ -1453,8 +1454,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", @@ -1617,6 +1617,7 @@ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", @@ -1631,6 +1632,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1649,6 +1651,7 @@ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" } @@ -2410,7 +2413,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2487,7 +2489,6 @@ "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tweetnacl": "^1.0.3" } @@ -2572,6 +2573,7 @@ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", @@ -2588,6 +2590,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2606,6 +2609,7 @@ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -2620,6 +2624,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2668,8 +2673,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -2738,7 +2742,6 @@ "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2838,7 +2841,6 @@ "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/compiler-sfc": "3.5.16", @@ -2861,6 +2863,7 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -2882,6 +2885,7 @@ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", "dev": true, + "peer": true, "engines": { "node": ">=0.4.0" } diff --git a/apps/main-dashboard/src/app/modules/dashboard/pages/complete-branding/complete-branding.ts b/apps/main-dashboard/src/app/modules/dashboard/pages/complete-branding/complete-branding.ts index 18855a53d..aaf28c692 100644 --- a/apps/main-dashboard/src/app/modules/dashboard/pages/complete-branding/complete-branding.ts +++ b/apps/main-dashboard/src/app/modules/dashboard/pages/complete-branding/complete-branding.ts @@ -164,6 +164,61 @@ export class CompleteBrandingPage implements OnInit { next: (project) => { this.project.set(project); this.isLoading.set(false); + + // Restaurer l'étape et l'état en fonction des données existantes + const branding = project?.analysisResultModel?.branding; + if (branding) { + const hasAiPreferences = !!branding.logoPreferences; + const hasGeneratedLogos = !!(branding.generatedLogos && branding.generatedLogos.length > 0); + + if (hasAiPreferences || hasGeneratedLogos) { + this.logoChoice.set('ai'); + if (branding.logoPreferences) { + this.aiLogoPreferences.set(branding.logoPreferences); + } + } else if (branding.logo) { + this.logoChoice.set('import'); + this.logoImportComplete.set(true); + } + + let targetStepIndex = 0; + const choice = this.logoChoice(); + + if (choice === 'import') { + if (branding.colors) this.colorSelected.set(true); + if (branding.typography) this.typographySelected.set(true); + + if (branding.logo && branding.colors && branding.typography) { + targetStepIndex = 3; // overview + } else if (branding.logo && branding.colors) { + targetStepIndex = 2; // typography + } else if (branding.logo) { + targetStepIndex = 1; // colors + } + } else if (choice === 'ai') { + if (branding.colors) this.colorSelected.set(true); + if (branding.typography) this.typographySelected.set(true); + + const hasLogo = !!branding.logo; + const hasVariations = !!branding.logo?.variations?.withText; + + if (hasLogo && hasVariations) { + targetStepIndex = 6; // overview + } else if (hasLogo) { + targetStepIndex = 5; // logo-variations + } else if (branding.logoPreferences) { + targetStepIndex = 4; // logo-selection + } else if (branding.typography) { + targetStepIndex = 3; // logo-preferences + } else if (branding.colors) { + targetStepIndex = 2; // typography + } else { + targetStepIndex = 1; // colors + } + } + + this.currentStepIndex.set(targetStepIndex); + } }, error: () => { this.isLoading.set(false); diff --git a/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-selection/logo-selection.ts b/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-selection/logo-selection.ts index 3a92d9260..f3483000b 100644 --- a/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-selection/logo-selection.ts +++ b/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-selection/logo-selection.ts @@ -154,6 +154,17 @@ export class LogoSelectionComponent implements OnInit, OnDestroy { customDescription: firstLogo.customDescription, }); } + + // Sélectionner automatiquement le logo existant s'il y en a un, sinon le premier logo + const currentSelectedLogo = this.project()?.analysisResultModel?.branding?.logo; + if (currentSelectedLogo && currentSelectedLogo.id) { + this.selectedLogoId.set(currentSelectedLogo.id); + } else if (this.logos()!.length > 0) { + // Retarder légèrement pour laisser le temps aux signaux de se stabiliser + setTimeout(() => { + this.selectLogo(this.logos()![0].id!); + }, 100); + } } } @@ -281,6 +292,11 @@ export class LogoSelectionComponent implements OnInit, OnDestroy { this.currentStep.set( this.translate.instant('dashboard.logoSelection.progress.completed'), ); + + // Sélectionner automatiquement le premier logo généré + if (logosWithUniqueIds.length > 0) { + this.selectLogo(logosWithUniqueIds[0].id); + } }, 1000); }, error: (error) => { diff --git a/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-variations/logo-variations.ts b/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-variations/logo-variations.ts index b79b62960..53b7446f0 100644 --- a/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-variations/logo-variations.ts +++ b/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/components/logo-variations/logo-variations.ts @@ -88,12 +88,50 @@ export class LogoVariationsComponent implements OnInit, OnDestroy { }; ngOnInit(): void { - // Auto-start generation when component loads - console.log(this.project().analysisResultModel.branding.logo.variations); - if (this.selectedLogo() && !this.project().analysisResultModel.branding.logo.variations) { + const existingVariations = this.project().analysisResultModel?.branding?.logo?.variations; + if (existingVariations) { + console.log('Using existing logo variations:', existingVariations); + const variations: DisplayVariation[] = []; + + if (existingVariations.withText) { + const withText = existingVariations.withText; + if (withText.lightBackground) { + variations.push({ + id: 'lightBackground', + background: 'lightBackground', + label: this.translate.instant('dashboard.logoVariations.labels.lightBackground'), + svgContent: withText.lightBackground, + description: this.translate.instant('dashboard.logoVariations.descriptions.lightBackground'), + backgroundColor: '#ffffff', + }); + } + if (withText.darkBackground) { + variations.push({ + id: 'darkBackground', + background: 'darkBackground', + label: this.translate.instant('dashboard.logoVariations.labels.darkBackground'), + svgContent: withText.darkBackground, + description: this.translate.instant('dashboard.logoVariations.descriptions.darkBackground'), + backgroundColor: '#1f2937', + }); + } + if (withText.monochrome) { + variations.push({ + id: 'monochrome', + background: 'monochrome', + label: this.translate.instant('dashboard.logoVariations.labels.monochrome'), + svgContent: withText.monochrome, + description: this.translate.instant('dashboard.logoVariations.descriptions.monochrome'), + backgroundColor: '#f3f4f6', + }); + } + } + + this.generatedVariations.set(variations); + this.isCompleted.set(true); + this.variationsGenerated.emit(existingVariations); + } else if (this.selectedLogo()) { this.startVariationGeneration(); - } else { - this.variationsGenerated.emit(this.project().analysisResultModel.branding.logo.variations!); } } diff --git a/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/create-project.ts b/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/create-project.ts index cf8045839..274a32198 100644 --- a/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/create-project.ts +++ b/apps/main-dashboard/src/app/modules/dashboard/pages/create-project/create-project.ts @@ -360,6 +360,7 @@ export class CreateProjectComponent implements OnInit { * Create project in database after details step */ private async createProjectInDatabase(): Promise { + if (this.project().id || this.isLoading()) return; try { this.isLoading.set(true); const currentProject = this.project(); diff --git a/apps/main-dashboard/src/app/modules/dashboard/services/ai-agents/branding.service.ts b/apps/main-dashboard/src/app/modules/dashboard/services/ai-agents/branding.service.ts index 379eafbec..995e7c958 100644 --- a/apps/main-dashboard/src/app/modules/dashboard/services/ai-agents/branding.service.ts +++ b/apps/main-dashboard/src/app/modules/dashboard/services/ai-agents/branding.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; -import { catchError, map, tap } from 'rxjs/operators'; +import { catchError, map, tap, timeout, retry } from 'rxjs/operators'; import { environment } from '../../../../../environments/environment'; import { BrandIdentityModel, ColorModel, TypographyModel } from '../../models/brand-identity.model'; import { ProjectModel } from '@idem/shared-models'; @@ -140,6 +140,8 @@ export class BrandingService { logos: LogoModel[]; }>(`${this.apiUrl}/generate/logo-concepts/${projectId}${force ? '?force=true' : ''}`, {}) .pipe( + timeout(180000), // 3 minutes timeout + retry(1), // retry once on transient network or CORS error tap((response) => console.log('generateLogosWithPreferences response:', response)), catchError((error) => { console.error('Error in generateLogosWithPreferences:', error); @@ -191,6 +193,8 @@ export class BrandingService { selectedLogo: selectedLogo, }) .pipe( + timeout(180000), // 3 minutes timeout + retry(1), // retry once tap((response) => console.log('generateLogoVariations response:', response)), catchError((error) => { console.error('Error in generateLogoVariations:', error); @@ -217,6 +221,8 @@ export class BrandingService { modificationPrompt: modificationPrompt, }) .pipe( + timeout(180000), // 3 minutes timeout + retry(1), // retry once tap((response) => console.log('editLogo response:', response)), catchError((error) => { console.error('Error in editLogo:', error); diff --git a/apps/main-dashboard/src/app/shared/interceptors/auth.interceptor.ts b/apps/main-dashboard/src/app/shared/interceptors/auth.interceptor.ts index c7d258738..4b3f0e9cb 100644 --- a/apps/main-dashboard/src/app/shared/interceptors/auth.interceptor.ts +++ b/apps/main-dashboard/src/app/shared/interceptors/auth.interceptor.ts @@ -3,7 +3,7 @@ import { PLATFORM_ID, inject } from '@angular/core'; import { TokenService } from '../services/token.service'; import { isPlatformServer } from '@angular/common'; import { Observable, from, of } from 'rxjs'; -import { switchMap, catchError } from 'rxjs/operators'; +import { switchMap, catchError, timeout } from 'rxjs/operators'; /** * Interceptor function to add JWT to requests. @@ -51,75 +51,85 @@ export const authInterceptor: HttpInterceptorFn = ( // real API requests, in a clean injection context. const tokenService = inject(TokenService); - // Wait for auth to be ready, then proceed with token logic - return from(tokenService.waitForAuthReady()).pipe( - switchMap(() => { - // First try to get cached token (fast path) - const cachedToken = tokenService.getToken(); + // 1. FAST PATH: If we already have a valid cached token, use it immediately + const cachedToken = tokenService.getToken(); + if (cachedToken) { + console.log('Auth Interceptor (Fast path): Using cached token for request:', req.url); + const authReq = req.clone({ + headers: req.headers.set('Authorization', `Bearer ${cachedToken}`), + }); - if (cachedToken) { - console.log('Auth Interceptor: Using cached token for request:', req.url); - const authReq = req.clone({ - headers: req.headers.set('Authorization', `Bearer ${cachedToken}`), - }); + return next(authReq).pipe( + catchError((error) => { + // If we get 401/403, the cached token might be expired/revoked + if (error.status === 401 || error.status === 403) { + console.log('Auth Interceptor (Fast path): Cached token failed, refreshing...'); + return from(tokenService.refreshToken()).pipe( + switchMap((refreshedToken: string | null) => { + if (!refreshedToken) { + console.error('Auth Interceptor: Token refresh failed'); + throw error; + } - return next(authReq).pipe( - catchError((error) => { - // If we get 401/403, the cached token might be expired - if (error.status === 401 || error.status === 403) { - console.log('Auth Interceptor: Cached token failed, refreshing from Firebase'); - return from(tokenService.refreshToken()).pipe( - switchMap((refreshedToken: string | null) => { - if (!refreshedToken) { - console.error('Auth Interceptor: Token refresh failed'); - throw error; - } + console.log('Auth Interceptor: Retrying request with refreshed token'); + const retryReq = req.clone({ + headers: req.headers.set('Authorization', `Bearer ${refreshedToken}`), + }); - console.log('Auth Interceptor: Retrying request with refreshed token'); - const retryReq = req.clone({ - headers: req.headers.set('Authorization', `Bearer ${refreshedToken}`), - }); + return next(retryReq); + }), + catchError(() => { + console.error('Auth Interceptor: Token refresh failed, throwing original error'); + throw error; + }), + ); + } + throw error; + }), + ); + } - return next(retryReq); - }), - catchError(() => { - console.error('Auth Interceptor: Token refresh failed, throwing original error'); - throw error; - }), - ); - } - throw error; - }), - ); + // 2. SLOW PATH: Wait for auth to be ready with a timeout + return from(tokenService.waitForAuthReady()).pipe( + timeout(5000), // 5 seconds timeout max to avoid hanging requests + switchMap(() => { + // Try to get cached token again (in case auth loaded it during wait) + const token = tokenService.getToken(); + if (token) { + const authReq = req.clone({ + headers: req.headers.set('Authorization', `Bearer ${token}`), + }); + return next(authReq); } - // No cached token, try to get one from Firebase (slow path) - console.log('Auth Interceptor: No cached token, getting from Firebase for request:', req.url); + // If still no token, try to fetch it async return from(tokenService.getTokenAsync()).pipe( - switchMap((token: string | null) => { - if (!token) { - console.warn( - 'Auth Interceptor: No authentication token available for request:', - req.url, - ); + switchMap((asyncToken: string | null) => { + if (!asyncToken) { + console.warn('Auth Interceptor: No auth token available after initialization'); return next(req); } - const authReq = req.clone({ - headers: req.headers.set('Authorization', `Bearer ${token}`), + headers: req.headers.set('Authorization', `Bearer ${asyncToken}`), }); - return next(authReq); }), - catchError((error) => { - console.error('Auth Interceptor: Error getting token from Firebase:', error); + catchError((asyncError) => { + console.error('Auth Interceptor: Error getting token async:', asyncError); return next(req); - }), + }) ); }), catchError((error) => { - console.error('Error waiting for auth ready in interceptor:', error); - // If auth ready fails, proceed without token + console.error('Auth Interceptor: Error waiting for auth ready or timeout:', error); + // Fallback: if we have a token stored, try it anyway before giving up + const fallbackToken = tokenService.getToken(); + if (fallbackToken) { + const authReq = req.clone({ + headers: req.headers.set('Authorization', `Bearer ${fallbackToken}`), + }); + return next(authReq); + } return next(req); }), );