fix 401 loop hell

This commit is contained in:
Chris Chen
2026-05-27 15:09:05 -07:00
parent e83fa4c2e9
commit d79b1faa8f
13 changed files with 196 additions and 90 deletions
+37 -15
View File
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { catchError, filter, finalize, map, shareReplay, take, tap } from 'rxjs/operators';
import { ApiConfigService } from '../../core/services/api-config.service';
// ── Public interfaces ─────────────────────────────────────────────────────────
@@ -69,6 +69,8 @@ export class AuthService {
/** Observable stream of the current user (null = not authenticated). */
public currentUser = this.currentUser$.asObservable();
private readonly sessionReady$ = new BehaviorSubject(false);
private refreshInFlight$: Observable<boolean> | null = null;
private redirectUrl = '/dashboard';
constructor(
@@ -118,18 +120,25 @@ export class AuthService {
* Never throws.
*/
refresh(): Observable<boolean> {
return this.http.post<ApiLoginResponse>(
`${this.apiConfig.authUrl}/refresh`,
{},
{ withCredentials: true }
).pipe(
tap(response => {
this.accessToken$.next(response.accessToken);
this.currentUser$.next(response.user);
}),
map(() => true),
catchError(() => of(false))
);
if (!this.refreshInFlight$) {
this.refreshInFlight$ = this.http.post<ApiLoginResponse>(
`${this.apiConfig.authUrl}/refresh`,
{},
{ withCredentials: true }
).pipe(
tap(response => {
this.accessToken$.next(response.accessToken);
this.currentUser$.next(response.user);
}),
map(() => true),
catchError(() => of(false)),
finalize(() => {
this.refreshInFlight$ = null;
}),
shareReplay(1)
);
}
return this.refreshInFlight$;
}
/**
@@ -155,10 +164,23 @@ export class AuthService {
*/
initializeFromRefreshToken(): Promise<void> {
return new Promise(resolve => {
this.refresh().subscribe(() => resolve());
this.refresh().pipe(
finalize(() => {
this.sessionReady$.next(true);
resolve();
})
).subscribe();
});
}
/** Resolves once startup session restore has finished (success or failure). */
whenSessionReady(): Observable<boolean> {
if (this.sessionReady$.value) {
return of(true);
}
return this.sessionReady$.pipe(filter(Boolean), take(1));
}
// ── State accessors ─────────────────────────────────────────────────────
getToken(): string | null {
@@ -166,7 +188,7 @@ export class AuthService {
}
isAuthenticated(): boolean {
return this.currentUser$.value !== null;
return this.currentUser$.value !== null && this.getToken() !== null;
}
getCurrentUser(): UserInfo | null {