import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError, of } from 'rxjs'; import { map, catchError, tap, retry } from 'rxjs/operators'; import { environment } from '../../environments/environment'; export interface AutheliaUser { username: string; displayName: string; email: string; groups: string[]; } export interface AutheliaSession { username: string; displayName: string; email: string; groups: string[]; authenticated: boolean; expires: string; } export interface AutheliaLoginRequest { username: string; password: string; keepMeLoggedIn?: boolean; targetURL?: string; } export interface AutheliaLoginResponse { status: 'OK' | 'FAILED'; message?: string; data?: { redirect?: string; }; } @Injectable({ providedIn: 'root' }) export class AutheliaAuthService { private readonly authBaseUrl = environment.authelia?.baseUrl || 'https://auth.gm-tech.org'; private isAuthenticatedSubject = new BehaviorSubject(false); private userSubject = new BehaviorSubject(null); private sessionSubject = new BehaviorSubject(null); public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); public user$ = this.userSubject.asObservable(); public session$ = this.sessionSubject.asObservable(); constructor(private http: HttpClient) { this.checkAuthenticationStatus(); } /** * Check if user is currently authenticated with Authelia */ public checkAuthenticationStatus(): Observable { const verifyUrl = `${this.authBaseUrl}/api/verify`; return this.http.get(verifyUrl, { withCredentials: true, headers: this.getHeaders() }).pipe( map(session => { if (session.authenticated) { this.sessionSubject.next(session); this.userSubject.next({ username: session.username, displayName: session.displayName, email: session.email, groups: session.groups }); this.isAuthenticatedSubject.next(true); console.log('✅ User authenticated with Authelia:', session.username); return true; } else { this.clearSession(); return false; } }), catchError((error: HttpErrorResponse) => { console.log('❌ Authentication check failed:', error.status); this.clearSession(); return of(false); }) ); } /** * Login with Authelia */ public login(username: string, password: string, targetURL?: string): Observable { const loginUrl = `${this.authBaseUrl}/api/firstfactor`; const loginData: AutheliaLoginRequest = { username, password, keepMeLoggedIn: true, targetURL: targetURL || environment.n8nWebhookUrl }; return this.http.post(loginUrl, loginData, { withCredentials: true, headers: this.getHeaders() }).pipe( tap(response => { if (response.status === 'OK') { console.log('✅ Login successful'); // Refresh authentication status this.checkAuthenticationStatus().subscribe(); } else { console.log('❌ Login failed:', response.message); } }), catchError(this.handleError) ); } /** * Logout from Authelia */ public logout(): Observable { const logoutUrl = `${this.authBaseUrl}/api/logout`; return this.http.post(logoutUrl, {}, { withCredentials: true, headers: this.getHeaders() }).pipe( tap(() => { console.log('✅ Logout successful'); this.clearSession(); }), catchError(this.handleError) ); } /** * Get authentication headers for API requests */ public getAuthHeaders(): HttpHeaders { return this.getHeaders(); } /** * Make authenticated request to protected resource */ public makeAuthenticatedRequest(url: string, data?: any, method: 'GET' | 'POST' = 'POST'): Observable { const options = { withCredentials: true, headers: this.getHeaders() }; if (method === 'GET') { return this.http.get(url, options).pipe( retry(1), catchError(this.handleAuthenticatedRequestError.bind(this)) ); } else { return this.http.post(url, data, options).pipe( retry(1), catchError(this.handleAuthenticatedRequestError.bind(this)) ); } } /** * Redirect to Authelia login page */ public redirectToLogin(targetURL?: string): void { const returnURL = targetURL || window.location.href; const loginURL = `${this.authBaseUrl}/?rd=${encodeURIComponent(returnURL)}`; window.location.href = loginURL; } /** * Get current authentication status */ public get isAuthenticated(): boolean { return this.isAuthenticatedSubject.value; } /** * Get current user */ public get currentUser(): AutheliaUser | null { return this.userSubject.value; } /** * Get current session */ public get currentSession(): AutheliaSession | null { return this.sessionSubject.value; } private getHeaders(): HttpHeaders { return new HttpHeaders({ 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }); } private clearSession(): void { this.isAuthenticatedSubject.next(false); this.userSubject.next(null); this.sessionSubject.next(null); } private handleAuthenticatedRequestError(error: HttpErrorResponse): Observable { if (error.status === 401 || error.status === 403) { console.log('🔒 Authentication required for protected resource'); this.clearSession(); // Don't automatically redirect for API calls, let the calling component handle it return throwError(() => new Error('Authentication required')); } return this.handleError(error); } private handleError(error: HttpErrorResponse): Observable { let errorMessage = 'An error occurred'; if (error.error instanceof ErrorEvent) { // Client-side error errorMessage = `Error: ${error.error.message}`; } else { // Server-side error errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`; } console.error('❌ Authelia service error:', errorMessage); return throwError(() => new Error(errorMessage)); } }