import { computed, Injectable, inject, signal } from '@angular/core'; import { BehaviorSubject, Observable, catchError, finalize, map, shareReplay, throwError } from 'rxjs'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { LoginRequest, LoginResponse, UserProfile } from '../../models/auth/auth.model'; import { TokenStorageService } from './token-storage.service'; import { AppContextService } from '../../services/context/app-context.service'; import { AUTH_ENDPOINTS } from '../../../core/end-points/auth/auth.endpoints'; @Injectable({ providedIn: 'root' }) export class AuthService { private readonly http = inject(HttpClient); private readonly tokenStorage = inject(TokenStorageService); private readonly appContextService = inject(AppContextService); private readonly userSubject = new BehaviorSubject(null); readonly user$ = this.userSubject.asObservable(); readonly currentUserSignal = signal(null); private readonly accessTokenSignal = signal(null); readonly isAuthenticatedSignal = computed(() => !!this.accessTokenSignal() && !!this.currentUserSignal()); private refreshRequest$: Observable | null = null; constructor() { this.restoreAuthState(); } login(payload: LoginRequest, rememberMe: boolean): Observable { return this.http.post(`${AUTH_ENDPOINTS.login}`, payload).pipe( map((response) => { const user = this.normalizeUserProfile(response); this.tokenStorage.saveAuth(response, rememberMe); this.setAuthState(user, this.tokenStorage.getAccessToken()); return response; }) ); } refreshAccessToken(): Observable { if (this.refreshRequest$) { return this.refreshRequest$; } const refreshToken = this.tokenStorage.getRefreshToken(); if (!refreshToken) { this.logout(); return throwError(() => new HttpErrorResponse({ status: 401, statusText: 'Refresh token missing' })); } this.refreshRequest$ = this.http.post(`${AUTH_ENDPOINTS.refresh}`, { refreshToken }).pipe( map((response) => { const user = this.normalizeUserProfile(response, this.currentUserSignal()); const storageType = this.tokenStorage.getStorageType(); const rememberMe = storageType === 'local'; this.tokenStorage.saveAuth(response, rememberMe); this.setAuthState(user, this.tokenStorage.getAccessToken()); return response; }), catchError((error) => { this.logout(); return throwError(() => error); }), finalize(() => { this.refreshRequest$ = null; }), shareReplay(1) ); return this.refreshRequest$; } logout(): void { this.tokenStorage.clearAuth(); this.appContextService.clearContext(); this.setAuthState(null, null); this.refreshRequest$ = null; } get accessToken(): string | null { return this.tokenStorage.getAccessToken(); } get refreshToken(): string | null { return this.tokenStorage.getRefreshToken(); } get isAuthenticated(): boolean { return this.isAuthenticatedSignal(); } get isLoggedIn(): boolean { return this.isAuthenticatedSignal(); } get currentUser(): UserProfile | null { return this.currentUserSignal(); } private restoreAuthState(): void { const storedUser = this.tokenStorage.getUser(); const storedAccessToken = this.tokenStorage.getAccessToken(); if (storedUser && storedAccessToken) { this.setAuthState(storedUser, storedAccessToken); return; } this.setAuthState(null, storedAccessToken ?? null); } private setAuthState(user: UserProfile | null, token: string | null): void { this.userSubject.next(user); this.currentUserSignal.set(user); this.accessTokenSignal.set(token); } private normalizeUserProfile(response: LoginResponse, fallbackUser: UserProfile | null = null): UserProfile | null { if (response.user) { return response.user; } const userId = response.userId?.trim(); const email = response.email?.trim(); if (userId || email) { return { id: userId ?? fallbackUser?.id ?? '', email: email ?? fallbackUser?.email ?? '', roles: response.roles, }; } return fallbackUser; } }