From 063092f2c72441515257a578c9c9e2f06aa9e57a Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Thu, 17 Sep 2026 14:40:53 +0200 Subject: [PATCH] fix(frontend): le rafraichissement de session au demarrage ne doit pas ecraser un lien de reset Le refresh de session lance par provideAppInitializer echoue silencieusement sans cookie valide, mais l'intercepteur forcait quand meme un router.navigate(['/login']) sur le 401 resultant, ecrasant la navigation vers /reset-password?token=... venue de l'email. L'intercepteur ne redirige plus quand on est deja sur une route invitee (login, forgot-password, reset-password). --- .../interceptors/auth-interceptor.spec.ts | 18 ++++++++++++++- .../app/core/interceptors/auth-interceptor.ts | 22 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/app/core/interceptors/auth-interceptor.spec.ts b/apps/frontend/src/app/core/interceptors/auth-interceptor.spec.ts index 8f74cd8..9064aaf 100644 --- a/apps/frontend/src/app/core/interceptors/auth-interceptor.spec.ts +++ b/apps/frontend/src/app/core/interceptors/auth-interceptor.spec.ts @@ -41,7 +41,10 @@ describe('authInterceptor', () => { httpMock = TestBed.inject(HttpTestingController); }); - afterEach(() => httpMock.verify()); + afterEach(() => { + httpMock.verify(); + vi.restoreAllMocks(); + }); it('ajoute le header Authorization quand un token est disponible', () => { http.get('/api/v1/stats/summary').subscribe(); @@ -97,6 +100,19 @@ describe('authInterceptor', () => { expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); }); + it("ne redirige pas vers /login sur un 401 de /auth/refresh si on est déjà sur /reset-password", () => { + vi.spyOn(window, 'location', 'get').mockReturnValue({ + pathname: '/reset-password', + } as Location); + + http.post('/api/v1/auth/refresh', {}).subscribe({ error: () => {} }); + const req = httpMock.expectOne('/api/v1/auth/refresh'); + req.flush({}, { status: 401, statusText: 'Unauthorized' }); + + expect(authMock.clearSession).toHaveBeenCalled(); + expect(routerMock.navigate).not.toHaveBeenCalled(); + }); + it('rafraîchit puis rejoue la requête sur un 401 avec error="expired"', () => { authMock.refreshShared.mockReturnValue(of({ access_token: 'new-token' })); authMock.getAccessToken.mockReturnValueOnce('old-token').mockReturnValue('new-token'); diff --git a/apps/frontend/src/app/core/interceptors/auth-interceptor.ts b/apps/frontend/src/app/core/interceptors/auth-interceptor.ts index 46ba124..16f3047 100644 --- a/apps/frontend/src/app/core/interceptors/auth-interceptor.ts +++ b/apps/frontend/src/app/core/interceptors/auth-interceptor.ts @@ -11,6 +11,16 @@ function parseAuthError(response: HttpErrorResponse): string | null { return match ? match[1] : null; } +const ROUTES_INVITEES = ['/login', '/forgot-password', '/reset-password']; + +// Piège : le rafraîchissement de session lancé au démarrage de l'app (provideAppInitializer) +// échoue silencieusement sans cookie valide. `window.location.pathname` (pas `router.url`, +// pas encore fiable à ce stade) évite qu'un 401 de fond écrase la navigation vers le lien de +// reset reçu par email. +function surRouteInvitee(): boolean { + return ROUTES_INVITEES.some((chemin) => window.location.pathname.startsWith(chemin)); +} + export const authInterceptor: HttpInterceptorFn = (req, next) => { const auth = inject(AuthService); const router = inject(Router); @@ -43,7 +53,9 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => { if (req.url.endsWith('/auth/refresh')) { auth.clearSession(); - router.navigate(['/login']); + if (!surRouteInvitee()) { + router.navigate(['/login']); + } return throwError(() => error); } @@ -51,7 +63,9 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => { if (kind === 'invalid_token') { auth.clearSession(); - router.navigate(['/login']); + if (!surRouteInvitee()) { + router.navigate(['/login']); + } return throwError(() => error); } @@ -65,7 +79,9 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => { }), catchError((refreshError) => { auth.clearSession(); - router.navigate(['/login']); + if (!surRouteInvitee()) { + router.navigate(['/login']); + } return throwError(() => refreshError); }) );