Spaces:
				
			
			
	
			
			
		Paused
		
	
	
	
			
			
	
	
	
	
		
		
		Paused
		
	| // auth.service.ts | |
| // Path: /flare-ui/src/app/services/auth.service.ts | |
| import { Injectable, inject } from '@angular/core'; | |
| import { HttpClient, HttpErrorResponse } from '@angular/common/http'; | |
| import { Router } from '@angular/router'; | |
| import { BehaviorSubject, Observable, throwError, of } from 'rxjs'; | |
| import { tap, catchError, map, timeout, retry } from 'rxjs/operators'; | |
| interface LoginResponse { | |
| token: string; | |
| username: string; | |
| expires_at?: string; | |
| refresh_token?: string; | |
| } | |
| interface AuthError { | |
| message: string; | |
| code?: string; | |
| details?: any; | |
| } | |
| ({ | |
| providedIn: 'root' | |
| }) | |
| export class AuthService { | |
| private http = inject(HttpClient); | |
| private router = inject(Router); | |
| private tokenKey = 'flare_token'; | |
| private usernameKey = 'flare_username'; | |
| private refreshTokenKey = 'flare_refresh_token'; | |
| private tokenExpiryKey = 'flare_token_expiry'; | |
| private loggedInSubject = new BehaviorSubject<boolean>(this.hasValidToken()); | |
| public loggedIn$ = this.loggedInSubject.asObservable(); | |
| private readonly REQUEST_TIMEOUT = 30000; // 30 seconds | |
| private tokenRefreshInProgress = false; | |
| private tokenRefreshSubject = new BehaviorSubject<string | null>(null); | |
| login(username: string, password: string): Observable<LoginResponse> { | |
| // Validate input | |
| if (!username || !password) { | |
| return throwError(() => ({ | |
| message: 'Username and password are required', | |
| code: 'VALIDATION_ERROR' | |
| } as AuthError)); | |
| } | |
| return this.http.post<LoginResponse>('/api/admin/login', { username, password }) | |
| .pipe( | |
| timeout(this.REQUEST_TIMEOUT), | |
| retry({ count: 2, delay: 1000 }), | |
| tap(response => { | |
| this.handleLoginSuccess(response); | |
| }), | |
| catchError(error => this.handleAuthError(error, 'login')) | |
| ); | |
| } | |
| logout(): void { | |
| try { | |
| // Clear all auth data | |
| this.clearAuthData(); | |
| // Update logged in state | |
| this.loggedInSubject.next(false); | |
| // Optional: Call logout endpoint | |
| this.http.post('/api/logout', {}).pipe( | |
| catchError(() => of(null)) // Ignore logout errors | |
| ).subscribe(); | |
| // Navigate to login | |
| this.router.navigate(['/login']); | |
| console.log('β User logged out successfully'); | |
| } catch (error) { | |
| console.error('Error during logout:', error); | |
| // Still navigate to login even if error occurs | |
| this.router.navigate(['/login']); | |
| } | |
| } | |
| refreshToken(): Observable<LoginResponse> { | |
| const refreshToken = this.getRefreshToken(); | |
| if (!refreshToken) { | |
| return throwError(() => ({ | |
| message: 'No refresh token available', | |
| code: 'NO_REFRESH_TOKEN' | |
| } as AuthError)); | |
| } | |
| // Prevent multiple simultaneous refresh requests | |
| if (this.tokenRefreshInProgress) { | |
| return this.tokenRefreshSubject.asObservable().pipe( | |
| map(token => { | |
| if (token) { | |
| return { token, username: this.getUsername() || '' } as LoginResponse; | |
| } | |
| throw new Error('Token refresh failed'); | |
| }) | |
| ); | |
| } | |
| this.tokenRefreshInProgress = true; | |
| return this.http.post<LoginResponse>('/api/refresh', { refresh_token: refreshToken }) | |
| .pipe( | |
| timeout(this.REQUEST_TIMEOUT), | |
| tap(response => { | |
| this.handleLoginSuccess(response); | |
| this.tokenRefreshSubject.next(response.token); | |
| this.tokenRefreshInProgress = false; | |
| }), | |
| catchError(error => { | |
| this.tokenRefreshInProgress = false; | |
| this.tokenRefreshSubject.next(null); | |
| // If refresh fails, logout user | |
| if (error.status === 401 || error.status === 403) { | |
| this.logout(); | |
| } | |
| return this.handleAuthError(error, 'refresh'); | |
| }) | |
| ); | |
| } | |
| getToken(): string | null { | |
| try { | |
| // Check if token is expired | |
| if (this.isTokenExpired()) { | |
| this.clearAuthData(); | |
| return null; | |
| } | |
| return localStorage.getItem(this.tokenKey); | |
| } catch (error) { | |
| console.error('Error getting token:', error); | |
| return null; | |
| } | |
| } | |
| getUsername(): string | null { | |
| try { | |
| return localStorage.getItem(this.usernameKey); | |
| } catch (error) { | |
| console.error('Error getting username:', error); | |
| return null; | |
| } | |
| } | |
| getRefreshToken(): string | null { | |
| try { | |
| return localStorage.getItem(this.refreshTokenKey); | |
| } catch (error) { | |
| console.error('Error getting refresh token:', error); | |
| return null; | |
| } | |
| } | |
| setToken(token: string): void { | |
| try { | |
| localStorage.setItem(this.tokenKey, token); | |
| } catch (error) { | |
| console.error('Error setting token:', error); | |
| throw new Error('Failed to save authentication token'); | |
| } | |
| } | |
| setUsername(username: string): void { | |
| try { | |
| localStorage.setItem(this.usernameKey, username); | |
| } catch (error) { | |
| console.error('Error setting username:', error); | |
| } | |
| } | |
| hasToken(): boolean { | |
| return !!this.getToken(); | |
| } | |
| hasValidToken(): boolean { | |
| return this.hasToken() && !this.isTokenExpired(); | |
| } | |
| isLoggedIn(): boolean { | |
| return this.hasValidToken(); | |
| } | |
| isTokenExpired(): boolean { | |
| try { | |
| const expiryStr = localStorage.getItem(this.tokenExpiryKey); | |
| if (!expiryStr) { | |
| return false; // No expiry means token doesn't expire | |
| } | |
| const expiry = new Date(expiryStr); | |
| return expiry <= new Date(); | |
| } catch (error) { | |
| console.error('Error checking token expiry:', error); | |
| return true; // Assume expired on error | |
| } | |
| } | |
| getTokenExpiry(): Date | null { | |
| try { | |
| const expiryStr = localStorage.getItem(this.tokenExpiryKey); | |
| return expiryStr ? new Date(expiryStr) : null; | |
| } catch (error) { | |
| console.error('Error getting token expiry:', error); | |
| return null; | |
| } | |
| } | |
| private handleLoginSuccess(response: LoginResponse): void { | |
| try { | |
| // Save auth data | |
| this.setToken(response.token); | |
| this.setUsername(response.username); | |
| if (response.refresh_token) { | |
| localStorage.setItem(this.refreshTokenKey, response.refresh_token); | |
| } | |
| if (response.expires_at) { | |
| localStorage.setItem(this.tokenExpiryKey, response.expires_at); | |
| } | |
| // Update logged in state | |
| this.loggedInSubject.next(true); | |
| console.log('β Login successful for user:', response.username); | |
| } catch (error) { | |
| console.error('Error handling login success:', error); | |
| throw new Error('Failed to save authentication data'); | |
| } | |
| } | |
| private clearAuthData(): void { | |
| try { | |
| localStorage.removeItem(this.tokenKey); | |
| localStorage.removeItem(this.usernameKey); | |
| localStorage.removeItem(this.refreshTokenKey); | |
| localStorage.removeItem(this.tokenExpiryKey); | |
| } catch (error) { | |
| console.error('Error clearing auth data:', error); | |
| } | |
| } | |
| private handleAuthError(error: HttpErrorResponse, operation: string): Observable<never> { | |
| console.error(`Auth error during ${operation}:`, error); | |
| let authError: AuthError; | |
| // Handle different error types | |
| if (error.status === 0) { | |
| // Network error | |
| authError = { | |
| message: 'Network error. Please check your connection.', | |
| code: 'NETWORK_ERROR' | |
| }; | |
| } else if (error.status === 401) { | |
| authError = { | |
| message: error.error?.message || 'Invalid credentials', | |
| code: 'UNAUTHORIZED' | |
| }; | |
| } else if (error.status === 403) { | |
| authError = { | |
| message: error.error?.message || 'Access forbidden', | |
| code: 'FORBIDDEN' | |
| }; | |
| } else if (error.status === 409) { | |
| // Race condition | |
| authError = { | |
| message: error.error?.message || 'Request conflict. Please try again.', | |
| code: 'CONFLICT', | |
| details: error.error?.details | |
| }; | |
| } else if (error.status === 422) { | |
| // Validation error | |
| authError = { | |
| message: error.error?.message || 'Validation error', | |
| code: 'VALIDATION_ERROR', | |
| details: error.error?.details | |
| }; | |
| } else if (error.status >= 500) { | |
| authError = { | |
| message: 'Server error. Please try again later.', | |
| code: 'SERVER_ERROR' | |
| }; | |
| } else { | |
| authError = { | |
| message: error.error?.message || error.message || 'Authentication failed', | |
| code: 'UNKNOWN_ERROR' | |
| }; | |
| } | |
| return throwError(() => authError); | |
| } | |
| // Validate current session | |
| validateSession(): Observable<boolean> { | |
| if (!this.hasToken()) { | |
| return of(false); | |
| } | |
| return this.http.get<{ valid: boolean }>('/api/validate') | |
| .pipe( | |
| timeout(this.REQUEST_TIMEOUT), | |
| map(response => response.valid), | |
| catchError(error => { | |
| if (error.status === 401) { | |
| this.clearAuthData(); | |
| this.loggedInSubject.next(false); | |
| } | |
| return of(false); | |
| }) | |
| ); | |
| } | |
| // Get user profile | |
| getUserProfile(): Observable<any> { | |
| return this.http.get('/api/user/profile') | |
| .pipe( | |
| timeout(this.REQUEST_TIMEOUT), | |
| catchError(error => this.handleAuthError(error, 'getUserProfile')) | |
| ); | |
| } | |
| // Update password | |
| updatePassword(currentPassword: string, newPassword: string): Observable<any> { | |
| return this.http.post('/api/user/password', { | |
| current_password: currentPassword, | |
| new_password: newPassword | |
| }).pipe( | |
| timeout(this.REQUEST_TIMEOUT), | |
| catchError(error => this.handleAuthError(error, 'updatePassword')) | |
| ); | |
| } | |
| } | 

