Skip to content

Commit 763c411

Browse files
committed
fix: user.service.ts now handling multiple states for auth management
1 parent 875f15a commit 763c411

1 file changed

Lines changed: 112 additions & 4 deletions

File tree

src/app/core/auth/services/user.service.ts

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,62 @@
11
import { Injectable } from '@angular/core';
2-
import { Observable, BehaviorSubject } from 'rxjs';
2+
import { Observable, BehaviorSubject, EMPTY, Subscription, timer } from 'rxjs';
33

44
import { JwtService } from './jwt.service';
5-
import { map, distinctUntilChanged, tap, shareReplay } from 'rxjs/operators';
6-
import { HttpClient } from '@angular/common/http';
5+
import { map, distinctUntilChanged, tap, shareReplay, catchError } from 'rxjs/operators';
6+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
77
import { User } from '../user.model';
88
import { Router } from '@angular/router';
99

10+
export type AuthState = 'authenticated' | 'unauthenticated' | 'unavailable' | 'loading';
11+
12+
/**
13+
* UserService - Manages authentication state for the current user.
14+
*
15+
* ## Endpoints
16+
*
17+
* This service uses GET /user (not /users/:id or /profiles/:username):
18+
* - GET /user → Returns the authenticated user's own data (JWT token identifies who you are)
19+
* - GET /profiles/:username → Different endpoint for viewing any user's public profile (see ProfileService)
20+
*
21+
* ## Auth States
22+
*
23+
* - 'loading': Initial state, checking if stored token is valid
24+
* - 'authenticated': Token valid, user data loaded
25+
* - 'unauthenticated': No token or token invalid (4XX error)
26+
* - 'unavailable': Server error (5XX), token kept for retry
27+
*
28+
* ## Error Handling on GET /user
29+
*
30+
* - 4XX errors (400, 401, 403, 404): Token is invalid → logout, clear token
31+
* - 5XX errors (500, 503) or network errors: Server is down → keep token, auto-retry
32+
*
33+
* The distinction matters: a 401 means "your token is bad" (clear it), while a 500
34+
* means "server is broken" (keep the token, it might work when server recovers).
35+
*
36+
* ## Auto-Retry
37+
*
38+
* When in 'unavailable' state, the service automatically retries with exponential
39+
* backoff: 2s → 4s → 8s → 16s → 16s → ... (capped at 16s, retries indefinitely).
40+
* The UI shows "Connecting..." while retrying. User can also just reload the page.
41+
*
42+
* ## Global 401 Handling
43+
*
44+
* For other endpoints (not /user), 401 errors are caught by errorInterceptor
45+
* which calls purgeAuth() - this handles "token expired mid-session" scenarios.
46+
*/
1047
@Injectable({ providedIn: 'root' })
1148
export class UserService {
1249
private currentUserSubject = new BehaviorSubject<User | null>(null);
1350
public currentUser = this.currentUserSubject.asObservable().pipe(distinctUntilChanged());
1451

52+
private authStateSubject = new BehaviorSubject<AuthState>('loading');
53+
public authState = this.authStateSubject.asObservable().pipe(distinctUntilChanged());
54+
1555
public isAuthenticated = this.currentUser.pipe(map(user => !!user));
1656

57+
private retryAttempt = 0;
58+
private retrySubscription: Subscription | null = null;
59+
1760
constructor(
1861
private readonly http: HttpClient,
1962
private readonly jwtService: JwtService,
@@ -39,12 +82,71 @@ export class UserService {
3982
return this.http.get<{ user: User }>('/user').pipe(
4083
tap({
4184
next: ({ user }) => this.setAuth(user),
42-
error: () => this.purgeAuth(),
85+
error: (err: HttpErrorResponse) => this.handleAuthError(err),
4386
}),
4487
shareReplay(1),
88+
catchError(() => EMPTY),
4589
);
4690
}
4791

92+
/**
93+
* Handle errors from /api/user endpoint
94+
* - 4XX errors: Token is invalid, logout the user
95+
* - 5XX/network errors: Server issue, enter "unavailable" mode (keep token, show placeholder)
96+
*/
97+
private handleAuthError(err: HttpErrorResponse): void {
98+
const status = err.status;
99+
100+
if (status >= 400 && status < 500) {
101+
// 4XX: Client error (invalid token, forbidden, etc.) - logout
102+
this.purgeAuth();
103+
} else {
104+
// 5XX or network error (status 0) - auth temporarily unavailable
105+
this.setAuthUnavailable();
106+
}
107+
}
108+
109+
/**
110+
* Set auth state to unavailable (server error, but keep token for retry)
111+
*/
112+
private setAuthUnavailable(): void {
113+
this.currentUserSubject.next(null);
114+
this.authStateSubject.next('unavailable');
115+
this.scheduleRetry();
116+
}
117+
118+
/**
119+
* Schedule auto-retry with exponential backoff: 2s, 4s, 8s, 16s, 16s, 16s...
120+
*/
121+
private scheduleRetry(): void {
122+
this.cancelRetry();
123+
124+
if (!this.jwtService.getToken()) {
125+
return; // No token, nothing to retry
126+
}
127+
128+
// Calculate delay: 2, 4, 8, 16, 16, 16... (capped at 16s)
129+
const delaySeconds = Math.min(2 * Math.pow(2, this.retryAttempt), 16);
130+
this.retryAttempt++;
131+
132+
this.retrySubscription = timer(delaySeconds * 1000).subscribe(() => {
133+
if (this.jwtService.getToken()) {
134+
this.authStateSubject.next('loading');
135+
this.getCurrentUser().subscribe();
136+
}
137+
});
138+
}
139+
140+
/**
141+
* Cancel any pending retry
142+
*/
143+
private cancelRetry(): void {
144+
if (this.retrySubscription) {
145+
this.retrySubscription.unsubscribe();
146+
this.retrySubscription = null;
147+
}
148+
}
149+
48150
update(user: Partial<User>): Observable<{ user: User }> {
49151
return this.http.put<{ user: User }>('/user', { user }).pipe(
50152
tap(({ user }) => {
@@ -54,12 +156,18 @@ export class UserService {
54156
}
55157

56158
setAuth(user: User): void {
159+
this.cancelRetry();
160+
this.retryAttempt = 0;
57161
this.jwtService.saveToken(user.token);
58162
this.currentUserSubject.next(user);
163+
this.authStateSubject.next('authenticated');
59164
}
60165

61166
purgeAuth(): void {
167+
this.cancelRetry();
168+
this.retryAttempt = 0;
62169
this.jwtService.destroyToken();
63170
this.currentUserSubject.next(null);
171+
this.authStateSubject.next('unauthenticated');
64172
}
65173
}

0 commit comments

Comments
 (0)