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).
This commit is contained in:
Johan LEROY
2026-09-17 14:40:53 +02:00
parent 921da48eb1
commit 063092f2c7
2 changed files with 36 additions and 4 deletions
@@ -41,7 +41,10 @@ describe('authInterceptor', () => {
httpMock = TestBed.inject(HttpTestingController); httpMock = TestBed.inject(HttpTestingController);
}); });
afterEach(() => httpMock.verify()); afterEach(() => {
httpMock.verify();
vi.restoreAllMocks();
});
it('ajoute le header Authorization quand un token est disponible', () => { it('ajoute le header Authorization quand un token est disponible', () => {
http.get('/api/v1/stats/summary').subscribe(); http.get('/api/v1/stats/summary').subscribe();
@@ -97,6 +100,19 @@ describe('authInterceptor', () => {
expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); 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"', () => { it('rafraîchit puis rejoue la requête sur un 401 avec error="expired"', () => {
authMock.refreshShared.mockReturnValue(of({ access_token: 'new-token' })); authMock.refreshShared.mockReturnValue(of({ access_token: 'new-token' }));
authMock.getAccessToken.mockReturnValueOnce('old-token').mockReturnValue('new-token'); authMock.getAccessToken.mockReturnValueOnce('old-token').mockReturnValue('new-token');
@@ -11,6 +11,16 @@ function parseAuthError(response: HttpErrorResponse): string | null {
return match ? match[1] : 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) => { export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService); const auth = inject(AuthService);
const router = inject(Router); const router = inject(Router);
@@ -43,7 +53,9 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (req.url.endsWith('/auth/refresh')) { if (req.url.endsWith('/auth/refresh')) {
auth.clearSession(); auth.clearSession();
router.navigate(['/login']); if (!surRouteInvitee()) {
router.navigate(['/login']);
}
return throwError(() => error); return throwError(() => error);
} }
@@ -51,7 +63,9 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (kind === 'invalid_token') { if (kind === 'invalid_token') {
auth.clearSession(); auth.clearSession();
router.navigate(['/login']); if (!surRouteInvitee()) {
router.navigate(['/login']);
}
return throwError(() => error); return throwError(() => error);
} }
@@ -65,7 +79,9 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
}), }),
catchError((refreshError) => { catchError((refreshError) => {
auth.clearSession(); auth.clearSession();
router.navigate(['/login']); if (!surRouteInvitee()) {
router.navigate(['/login']);
}
return throwError(() => refreshError); return throwError(() => refreshError);
}) })
); );