import { Injectable, Inject, Optional } from '@angular/core'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, map, tap } from 'rxjs/operators'; import { HttpBackend, HttpClient } from '@angular/common/http'; import { IRoleActive } from '../public-api'; import { AUTH_CONFIG, IAuthConfig } from '../interfaces/auth-config.interface'; @Injectable({ providedIn: 'root' }) export class AuthService { private readonly TOKEN_KEY = 'authToken'; private readonly REFRESH_TOKEN_KEY = 'refreshToken'; private readonly ROLE_ACTIVE_KEY = 'role-active'; private tokenSubject = new BehaviorSubject(this.getToken()); /** Indica si hay un refresh en curso para serializar peticiones concurrentes */ isRefreshing = false; /** Emite el nuevo token cuando el refresh finaliza; null mientras está en curso */ refreshTokenSubject = new BehaviorSubject(null); private http: HttpClient; constructor( handler: HttpBackend, @Optional() @Inject(AUTH_CONFIG) private authConfig: IAuthConfig | null ) { // HttpClient construido sobre HttpBackend para evitar el bucle // con AuthInterceptor al hacer la llamada de refresh this.http = new HttpClient(handler); } // ── Token de acceso ────────────────────────────────────────────────────── setToken(token: string): void { localStorage.setItem(this.TOKEN_KEY, token); this.tokenSubject.next(token); } getToken(): string | null { return localStorage.getItem(this.TOKEN_KEY); } clearToken(): void { localStorage.removeItem(this.TOKEN_KEY); this.tokenSubject.next(null); } getTokenObservable(): Observable { return this.tokenSubject.asObservable(); } isLoggedIn(): boolean { return !!this.getToken(); } /** * Parsea el JWT y comprueba si el claim `exp` ya venció. * Retorna true si no hay token, si no es un JWT válido o si expiró. */ isTokenExpired(): boolean { const token = this.getToken(); if (!token) return true; try { const payload = JSON.parse(atob(token.split('.')[1])); return payload.exp * 1000 < Date.now(); } catch { return true; } } // ── Refresh token ──────────────────────────────────────────────────────── setRefreshToken(token: string): void { localStorage.setItem(this.REFRESH_TOKEN_KEY, token); } getRefreshToken(): string | null { return localStorage.getItem(this.REFRESH_TOKEN_KEY); } clearRefreshToken(): void { localStorage.removeItem(this.REFRESH_TOKEN_KEY); } /** * Llama al endpoint configurado en `AUTH_CONFIG.refresh_url` usando * HttpBackend (sin pasar por AuthInterceptor) y actualiza el token almacenado. */ refreshAccessToken(): Observable { const refreshUrl = this.authConfig?.refresh_url; const tokenKey = this.authConfig?.refresh_token_key ?? 'access_token'; const refreshToken = this.getRefreshToken(); if (!refreshUrl || !refreshToken) { return throwError(() => new Error('AUTH_CONFIG.refresh_url o refreshToken no configurados')); } return this.http.post>(refreshUrl, { refreshToken }).pipe( tap(response => { const newToken = response[tokenKey]; this.setToken(newToken); this.refreshTokenSubject.next(newToken); }), map(response => response[tokenKey]), catchError(error => { this.clearToken(); this.clearRefreshToken(); return throwError(() => error); }) ); } // ── Rol activo ─────────────────────────────────────────────────────────── removeActiveRole() { localStorage.removeItem(this.ROLE_ACTIVE_KEY); } setActiveRole(roleCode: IRoleActive) { localStorage.setItem(this.ROLE_ACTIVE_KEY, JSON.stringify(roleCode)); } getActiveRole(): IRoleActive | null { const sessionRoleActive = localStorage.getItem(this.ROLE_ACTIVE_KEY) || ''; return sessionRoleActive ? JSON.parse(sessionRoleActive) : null; } // ── Subdominio ─────────────────────────────────────────────────────────── getSubDomain(): string { return window.location.hostname.split('.')[0]; } }