flare / flare-ui /src /app /services /auth.service.ts
ciyidogan's picture
Upload 118 files
9f79da5 verified
// 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;
}
@Injectable({
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'))
);
}
}