import { Injectable } from '@angular/core'; import { Observable, BehaviorSubject, throwError, timer, EMPTY } from 'rxjs'; import { switchMap, tap, catchError, filter, take, shareReplay } from 'rxjs/operators'; import { AuthHttpService } from './auth-http.service'; import { TokenService } from './token.service'; import { LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, User, TokenPair, LogoutRequest, PasswordResetRequest, PasswordResetConfirmRequest, ChangePasswordRequest, EmailVerificationRequest, ResendVerificationRequest, TwoFactorSetupResponse, TwoFactorVerifyRequest, TwoFactorStatusResponse, ApiResponse, ApiError } from '../models/auth.models'; @Injectable({ providedIn: 'root' }) export class AuthService { private currentUserSubject = new BehaviorSubject(null); private isAuthenticatedSubject = new BehaviorSubject(false); private isLoadingSubject = new BehaviorSubject(false); public currentUser$ = this.currentUserSubject.asObservable(); public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); public isLoading$ = this.isLoadingSubject.asObservable(); private refreshTimer?: any; constructor( private authHttpService: AuthHttpService, private tokenService: TokenService ) { this.initialize(); } /** * Initialize auth service */ private initialize(): void { // Initialize storage listener for cross-tab sync this.tokenService.initStorageListener(); // Subscribe to token changes this.tokenService.token$.subscribe(token => { const isAuthenticated = !!token && this.tokenService.isTokenValid(); this.isAuthenticatedSubject.next(isAuthenticated); if (isAuthenticated) { this.loadCurrentUser(); this.scheduleTokenRefresh(); } else { this.currentUserSubject.next(null); this.clearRefreshTimer(); } }); // Check initial authentication state if (this.tokenService.isTokenValid()) { this.isAuthenticatedSubject.next(true); this.loadCurrentUser(); this.scheduleTokenRefresh(); } } /** * Configure auth service with base URL */ configure(baseUrl: string): void { this.authHttpService.setBaseUrl(baseUrl); } // Authentication Methods /** * Register new user */ register(request: RegisterRequest): Observable { this.isLoadingSubject.next(true); return this.authHttpService.register(request).pipe( tap(response => { this.tokenService.setTokens(response); this.currentUserSubject.next(response.user); this.isAuthenticatedSubject.next(true); this.scheduleTokenRefresh(); }), catchError(error => { this.isLoadingSubject.next(false); return throwError(() => error); }), tap(() => this.isLoadingSubject.next(false)) ); } /** * Login user */ login(request: LoginRequest): Observable { this.isLoadingSubject.next(true); return this.authHttpService.login(request).pipe( tap(response => { this.tokenService.setTokens(response); this.currentUserSubject.next(response.user); this.isAuthenticatedSubject.next(true); this.scheduleTokenRefresh(); }), catchError(error => { this.isLoadingSubject.next(false); return throwError(() => error); }), tap(() => this.isLoadingSubject.next(false)) ); } /** * Logout user */ logout(revokeRefreshToken = true): Observable { this.isLoadingSubject.next(true); const logoutRequest: LogoutRequest = revokeRefreshToken ? { refresh_token: this.tokenService.getRefreshToken() || undefined } : {}; return this.authHttpService.logout(logoutRequest).pipe( tap(() => { this.clearAuthState(); }), catchError(error => { // Even if logout fails on server, clear local state this.clearAuthState(); this.isLoadingSubject.next(false); return throwError(() => error); }), tap(() => this.isLoadingSubject.next(false)) ); } /** * Silently logout (clear local state only) */ logoutSilently(): void { this.clearAuthState(); } /** * Refresh access token */ refreshToken(): Observable { const refreshToken = this.tokenService.getRefreshToken(); if (!refreshToken) { this.logoutSilently(); return throwError(() => ({ error: 'No refresh token available' } as ApiError)); } return this.authHttpService.refreshToken({ refresh_token: refreshToken }).pipe( tap(tokenPair => { this.tokenService.setTokens(tokenPair); this.scheduleTokenRefresh(); }), catchError(error => { // If refresh fails, logout user this.logoutSilently(); return throwError(() => error); }) ); } /** * Get current user information */ getCurrentUser(): Observable { if (this.currentUserSubject.value) { return this.currentUserSubject.asObservable().pipe( filter(user => !!user), take(1) ); } return this.loadCurrentUser(); } /** * Load current user from server */ private loadCurrentUser(): Observable { return this.authHttpService.getCurrentUser().pipe( tap(user => this.currentUserSubject.next(user)), catchError(error => { // If getting current user fails, might be invalid token if (error.status === 401) { this.logoutSilently(); } return throwError(() => error); }), shareReplay(1) ); } // User Management /** * Update user profile */ updateProfile(updates: Partial): Observable { return this.authHttpService.updateUserProfile(updates).pipe( tap(user => this.currentUserSubject.next(user)) ); } /** * Change password */ changePassword(request: ChangePasswordRequest): Observable { return this.authHttpService.changePassword(request); } /** * Request password reset */ forgotPassword(request: PasswordResetRequest): Observable { return this.authHttpService.forgotPassword(request); } /** * Reset password with token */ resetPassword(request: PasswordResetConfirmRequest): Observable { return this.authHttpService.resetPassword(request); } /** * Verify email */ verifyEmail(request: EmailVerificationRequest): Observable { return this.authHttpService.verifyEmail(request); } /** * Resend email verification */ resendEmailVerification(request?: ResendVerificationRequest): Observable { return this.authHttpService.resendEmailVerification(request); } // Two-Factor Authentication /** * Setup 2FA */ setup2FA(): Observable { return this.authHttpService.setup2FA(); } /** * Verify 2FA setup */ verify2FASetup(request: TwoFactorVerifyRequest): Observable { return this.authHttpService.verify2FASetup(request); } /** * Disable 2FA */ disable2FA(): Observable { return this.authHttpService.disable2FA(); } /** * Get 2FA status */ get2FAStatus(): Observable { return this.authHttpService.get2FAStatus(); } // Token Management /** * Check if user is authenticated */ isAuthenticated(): boolean { return this.isAuthenticatedSubject.value; } /** * Get access token */ getAccessToken(): string | null { return this.tokenService.getAccessToken(); } /** * Check if user has specific scope */ hasScope(scope: string): boolean { return this.tokenService.hasScope(scope); } /** * Check if user has any of the specified scopes */ hasAnyScope(scopes: string[]): boolean { return this.tokenService.hasAnyScope(scopes); } /** * Check if user has all of the specified scopes */ hasAllScopes(scopes: string[]): boolean { return this.tokenService.hasAllScopes(scopes); } /** * Get user scopes */ getUserScopes(): string[] { return this.tokenService.getUserScopes(); } /** * Get user ID from token */ getUserId(): string | null { return this.tokenService.getUserId(); } /** * Get user email from token */ getUserEmail(): string | null { return this.tokenService.getUserEmail(); } /** * Get user organization from token */ getUserOrganization(): string | null { return this.tokenService.getUserOrganization(); } // Private Methods /** * Clear authentication state */ private clearAuthState(): void { this.tokenService.clearTokens(); this.currentUserSubject.next(null); this.isAuthenticatedSubject.next(false); this.clearRefreshTimer(); } /** * Schedule automatic token refresh */ private scheduleTokenRefresh(): void { this.clearRefreshTimer(); const timeUntilExpiry = this.tokenService.getTimeUntilExpiry(); if (timeUntilExpiry <= 0) return; // Refresh token 2 minutes before expiry const refreshTime = Math.max(1000, timeUntilExpiry - (2 * 60 * 1000)); this.refreshTimer = timer(refreshTime).pipe( switchMap(() => { if (this.tokenService.isTokenExpiringSoon()) { return this.refreshToken(); } return EMPTY; }), catchError(error => { console.warn('Auto token refresh failed:', error); return EMPTY; }) ).subscribe(); } /** * Clear refresh timer */ private clearRefreshTimer(): void { if (this.refreshTimer) { this.refreshTimer.unsubscribe(); this.refreshTimer = undefined; } } /** * Check service health */ healthCheck(): Observable { return this.authHttpService.healthCheck(); } }