Spaces:
Building
Building
// 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')) | |
); | |
} | |
} |