ciyidogan commited on
Commit
881ca77
·
verified ·
1 Parent(s): 56ea350

Update flare-ui/src/app/services/auth.service.ts

Browse files
flare-ui/src/app/services/auth.service.ts CHANGED
@@ -1,11 +1,23 @@
 
 
 
1
  import { Injectable, inject } from '@angular/core';
2
- import { HttpClient } from '@angular/common/http';
3
  import { Router } from '@angular/router';
4
- import { BehaviorSubject, Observable, tap } from 'rxjs';
 
5
 
6
  interface LoginResponse {
7
  token: string;
8
  username: string;
 
 
 
 
 
 
 
 
9
  }
10
 
11
  @Injectable({
@@ -17,49 +29,316 @@ export class AuthService {
17
 
18
  private tokenKey = 'flare_token';
19
  private usernameKey = 'flare_username';
 
 
20
 
21
- private loggedInSubject = new BehaviorSubject<boolean>(this.hasToken());
22
  public loggedIn$ = this.loggedInSubject.asObservable();
 
 
 
 
23
 
24
  login(username: string, password: string): Observable<LoginResponse> {
 
 
 
 
 
 
 
 
25
  return this.http.post<LoginResponse>('/api/login', { username, password })
26
  .pipe(
 
 
27
  tap(response => {
28
- this.setToken(response.token);
29
- this.setUsername(response.username);
30
- this.loggedInSubject.next(true);
31
- })
32
  );
33
  }
34
 
35
  logout(): void {
36
- localStorage.removeItem(this.tokenKey);
37
- localStorage.removeItem(this.usernameKey);
38
- this.loggedInSubject.next(false);
39
- this.router.navigate(['/login']);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
  getToken(): string | null {
43
- return localStorage.getItem(this.tokenKey);
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  getUsername(): string | null {
47
- return localStorage.getItem(this.usernameKey);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
  setToken(token: string): void {
51
- localStorage.setItem(this.tokenKey, token);
 
 
 
 
 
52
  }
53
 
54
  setUsername(username: string): void {
55
- localStorage.setItem(this.usernameKey, username);
 
 
 
 
56
  }
57
 
58
  hasToken(): boolean {
59
  return !!this.getToken();
60
  }
61
 
 
 
 
 
62
  isLoggedIn(): boolean {
63
- return this.hasToken();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
  }
 
1
+ // auth.service.ts
2
+ // Path: /flare-ui/src/app/services/auth.service.ts
3
+
4
  import { Injectable, inject } from '@angular/core';
5
+ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
6
  import { Router } from '@angular/router';
7
+ import { BehaviorSubject, Observable, throwError, of } from 'rxjs';
8
+ import { tap, catchError, map, timeout, retry } from 'rxjs/operators';
9
 
10
  interface LoginResponse {
11
  token: string;
12
  username: string;
13
+ expires_at?: string;
14
+ refresh_token?: string;
15
+ }
16
+
17
+ interface AuthError {
18
+ message: string;
19
+ code?: string;
20
+ details?: any;
21
  }
22
 
23
  @Injectable({
 
29
 
30
  private tokenKey = 'flare_token';
31
  private usernameKey = 'flare_username';
32
+ private refreshTokenKey = 'flare_refresh_token';
33
+ private tokenExpiryKey = 'flare_token_expiry';
34
 
35
+ private loggedInSubject = new BehaviorSubject<boolean>(this.hasValidToken());
36
  public loggedIn$ = this.loggedInSubject.asObservable();
37
+
38
+ private readonly REQUEST_TIMEOUT = 30000; // 30 seconds
39
+ private tokenRefreshInProgress = false;
40
+ private tokenRefreshSubject = new BehaviorSubject<string | null>(null);
41
 
42
  login(username: string, password: string): Observable<LoginResponse> {
43
+ // Validate input
44
+ if (!username || !password) {
45
+ return throwError(() => ({
46
+ message: 'Username and password are required',
47
+ code: 'VALIDATION_ERROR'
48
+ } as AuthError));
49
+ }
50
+
51
  return this.http.post<LoginResponse>('/api/login', { username, password })
52
  .pipe(
53
+ timeout(this.REQUEST_TIMEOUT),
54
+ retry({ count: 2, delay: 1000 }),
55
  tap(response => {
56
+ this.handleLoginSuccess(response);
57
+ }),
58
+ catchError(error => this.handleAuthError(error, 'login'))
 
59
  );
60
  }
61
 
62
  logout(): void {
63
+ try {
64
+ // Clear all auth data
65
+ this.clearAuthData();
66
+
67
+ // Update logged in state
68
+ this.loggedInSubject.next(false);
69
+
70
+ // Optional: Call logout endpoint
71
+ this.http.post('/api/logout', {}).pipe(
72
+ catchError(() => of(null)) // Ignore logout errors
73
+ ).subscribe();
74
+
75
+ // Navigate to login
76
+ this.router.navigate(['/login']);
77
+
78
+ console.log('✅ User logged out successfully');
79
+ } catch (error) {
80
+ console.error('Error during logout:', error);
81
+ // Still navigate to login even if error occurs
82
+ this.router.navigate(['/login']);
83
+ }
84
+ }
85
+
86
+ refreshToken(): Observable<LoginResponse> {
87
+ const refreshToken = this.getRefreshToken();
88
+
89
+ if (!refreshToken) {
90
+ return throwError(() => ({
91
+ message: 'No refresh token available',
92
+ code: 'NO_REFRESH_TOKEN'
93
+ } as AuthError));
94
+ }
95
+
96
+ // Prevent multiple simultaneous refresh requests
97
+ if (this.tokenRefreshInProgress) {
98
+ return this.tokenRefreshSubject.asObservable().pipe(
99
+ map(token => {
100
+ if (token) {
101
+ return { token, username: this.getUsername() || '' } as LoginResponse;
102
+ }
103
+ throw new Error('Token refresh failed');
104
+ })
105
+ );
106
+ }
107
+
108
+ this.tokenRefreshInProgress = true;
109
+
110
+ return this.http.post<LoginResponse>('/api/refresh', { refresh_token: refreshToken })
111
+ .pipe(
112
+ timeout(this.REQUEST_TIMEOUT),
113
+ tap(response => {
114
+ this.handleLoginSuccess(response);
115
+ this.tokenRefreshSubject.next(response.token);
116
+ this.tokenRefreshInProgress = false;
117
+ }),
118
+ catchError(error => {
119
+ this.tokenRefreshInProgress = false;
120
+ this.tokenRefreshSubject.next(null);
121
+
122
+ // If refresh fails, logout user
123
+ if (error.status === 401 || error.status === 403) {
124
+ this.logout();
125
+ }
126
+
127
+ return this.handleAuthError(error, 'refresh');
128
+ })
129
+ );
130
  }
131
 
132
  getToken(): string | null {
133
+ try {
134
+ // Check if token is expired
135
+ if (this.isTokenExpired()) {
136
+ this.clearAuthData();
137
+ return null;
138
+ }
139
+
140
+ return localStorage.getItem(this.tokenKey);
141
+ } catch (error) {
142
+ console.error('Error getting token:', error);
143
+ return null;
144
+ }
145
  }
146
 
147
  getUsername(): string | null {
148
+ try {
149
+ return localStorage.getItem(this.usernameKey);
150
+ } catch (error) {
151
+ console.error('Error getting username:', error);
152
+ return null;
153
+ }
154
+ }
155
+
156
+ getRefreshToken(): string | null {
157
+ try {
158
+ return localStorage.getItem(this.refreshTokenKey);
159
+ } catch (error) {
160
+ console.error('Error getting refresh token:', error);
161
+ return null;
162
+ }
163
  }
164
 
165
  setToken(token: string): void {
166
+ try {
167
+ localStorage.setItem(this.tokenKey, token);
168
+ } catch (error) {
169
+ console.error('Error setting token:', error);
170
+ throw new Error('Failed to save authentication token');
171
+ }
172
  }
173
 
174
  setUsername(username: string): void {
175
+ try {
176
+ localStorage.setItem(this.usernameKey, username);
177
+ } catch (error) {
178
+ console.error('Error setting username:', error);
179
+ }
180
  }
181
 
182
  hasToken(): boolean {
183
  return !!this.getToken();
184
  }
185
 
186
+ hasValidToken(): boolean {
187
+ return this.hasToken() && !this.isTokenExpired();
188
+ }
189
+
190
  isLoggedIn(): boolean {
191
+ return this.hasValidToken();
192
+ }
193
+
194
+ isTokenExpired(): boolean {
195
+ try {
196
+ const expiryStr = localStorage.getItem(this.tokenExpiryKey);
197
+ if (!expiryStr) {
198
+ return false; // No expiry means token doesn't expire
199
+ }
200
+
201
+ const expiry = new Date(expiryStr);
202
+ return expiry <= new Date();
203
+ } catch (error) {
204
+ console.error('Error checking token expiry:', error);
205
+ return true; // Assume expired on error
206
+ }
207
+ }
208
+
209
+ getTokenExpiry(): Date | null {
210
+ try {
211
+ const expiryStr = localStorage.getItem(this.tokenExpiryKey);
212
+ return expiryStr ? new Date(expiryStr) : null;
213
+ } catch (error) {
214
+ console.error('Error getting token expiry:', error);
215
+ return null;
216
+ }
217
+ }
218
+
219
+ private handleLoginSuccess(response: LoginResponse): void {
220
+ try {
221
+ // Save auth data
222
+ this.setToken(response.token);
223
+ this.setUsername(response.username);
224
+
225
+ if (response.refresh_token) {
226
+ localStorage.setItem(this.refreshTokenKey, response.refresh_token);
227
+ }
228
+
229
+ if (response.expires_at) {
230
+ localStorage.setItem(this.tokenExpiryKey, response.expires_at);
231
+ }
232
+
233
+ // Update logged in state
234
+ this.loggedInSubject.next(true);
235
+
236
+ console.log('✅ Login successful for user:', response.username);
237
+ } catch (error) {
238
+ console.error('Error handling login success:', error);
239
+ throw new Error('Failed to save authentication data');
240
+ }
241
+ }
242
+
243
+ private clearAuthData(): void {
244
+ try {
245
+ localStorage.removeItem(this.tokenKey);
246
+ localStorage.removeItem(this.usernameKey);
247
+ localStorage.removeItem(this.refreshTokenKey);
248
+ localStorage.removeItem(this.tokenExpiryKey);
249
+ } catch (error) {
250
+ console.error('Error clearing auth data:', error);
251
+ }
252
+ }
253
+
254
+ private handleAuthError(error: HttpErrorResponse, operation: string): Observable<never> {
255
+ console.error(`Auth error during ${operation}:`, error);
256
+
257
+ let authError: AuthError;
258
+
259
+ // Handle different error types
260
+ if (error.status === 0) {
261
+ // Network error
262
+ authError = {
263
+ message: 'Network error. Please check your connection.',
264
+ code: 'NETWORK_ERROR'
265
+ };
266
+ } else if (error.status === 401) {
267
+ authError = {
268
+ message: error.error?.message || 'Invalid credentials',
269
+ code: 'UNAUTHORIZED'
270
+ };
271
+ } else if (error.status === 403) {
272
+ authError = {
273
+ message: error.error?.message || 'Access forbidden',
274
+ code: 'FORBIDDEN'
275
+ };
276
+ } else if (error.status === 409) {
277
+ // Race condition
278
+ authError = {
279
+ message: error.error?.message || 'Request conflict. Please try again.',
280
+ code: 'CONFLICT',
281
+ details: error.error?.details
282
+ };
283
+ } else if (error.status === 422) {
284
+ // Validation error
285
+ authError = {
286
+ message: error.error?.message || 'Validation error',
287
+ code: 'VALIDATION_ERROR',
288
+ details: error.error?.details
289
+ };
290
+ } else if (error.status >= 500) {
291
+ authError = {
292
+ message: 'Server error. Please try again later.',
293
+ code: 'SERVER_ERROR'
294
+ };
295
+ } else {
296
+ authError = {
297
+ message: error.error?.message || error.message || 'Authentication failed',
298
+ code: 'UNKNOWN_ERROR'
299
+ };
300
+ }
301
+
302
+ return throwError(() => authError);
303
+ }
304
+
305
+ // Validate current session
306
+ validateSession(): Observable<boolean> {
307
+ if (!this.hasToken()) {
308
+ return of(false);
309
+ }
310
+
311
+ return this.http.get<{ valid: boolean }>('/api/validate')
312
+ .pipe(
313
+ timeout(this.REQUEST_TIMEOUT),
314
+ map(response => response.valid),
315
+ catchError(error => {
316
+ if (error.status === 401) {
317
+ this.clearAuthData();
318
+ this.loggedInSubject.next(false);
319
+ }
320
+ return of(false);
321
+ })
322
+ );
323
+ }
324
+
325
+ // Get user profile
326
+ getUserProfile(): Observable<any> {
327
+ return this.http.get('/api/user/profile')
328
+ .pipe(
329
+ timeout(this.REQUEST_TIMEOUT),
330
+ catchError(error => this.handleAuthError(error, 'getUserProfile'))
331
+ );
332
+ }
333
+
334
+ // Update password
335
+ updatePassword(currentPassword: string, newPassword: string): Observable<any> {
336
+ return this.http.post('/api/user/password', {
337
+ current_password: currentPassword,
338
+ new_password: newPassword
339
+ }).pipe(
340
+ timeout(this.REQUEST_TIMEOUT),
341
+ catchError(error => this.handleAuthError(error, 'updatePassword'))
342
+ );
343
  }
344
  }