interview-assistant/src/app/services/authelia-auth.service.ts
Interview Assistant Developer 326a4aaa29 Initial commit: Real-Time Interview Assistant with Authelia Authentication
🎯 Features implemented:
- Multi-language speech recognition (EN, FR, ES, DE)
- CV upload and parsing with regex-escaped skills extraction
- Authelia authentication integration for n8n webhook
- Complete n8n workflow for AI question generation
- Real-time language switching with enhanced UI
- Professional authentication modal with dual login options

🔧 Technical stack:
- Angular 18 with standalone components and signals
- TypeScript with strict typing and interfaces
- Authelia session-based authentication
- n8n workflow automation with OpenAI integration
- PDF.js for CV text extraction
- Web Speech API for voice recognition

🛠️ Infrastructure:
- Secure authentication flow with proper error handling
- Environment-based configuration for dev/prod
- Comprehensive documentation and workflow templates
- Clean project structure with proper git ignore rules

🔒 Security features:
- Cookie-based session management with CORS
- Protected n8n webhooks via Authelia
- Graceful fallback to local processing
- Secure redirect handling and session persistence

🚀 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 15:16:13 +00:00

238 lines
6.4 KiB
TypeScript

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<boolean>(false);
private userSubject = new BehaviorSubject<AutheliaUser | null>(null);
private sessionSubject = new BehaviorSubject<AutheliaSession | null>(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<boolean> {
const verifyUrl = `${this.authBaseUrl}/api/verify`;
return this.http.get<AutheliaSession>(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<AutheliaLoginResponse> {
const loginUrl = `${this.authBaseUrl}/api/firstfactor`;
const loginData: AutheliaLoginRequest = {
username,
password,
keepMeLoggedIn: true,
targetURL: targetURL || environment.n8nWebhookUrl
};
return this.http.post<AutheliaLoginResponse>(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<any> {
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<T>(url: string, data?: any, method: 'GET' | 'POST' = 'POST'): Observable<T> {
const options = {
withCredentials: true,
headers: this.getHeaders()
};
if (method === 'GET') {
return this.http.get<T>(url, options).pipe(
retry(1),
catchError(this.handleAuthenticatedRequestError.bind(this))
);
} else {
return this.http.post<T>(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<never> {
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<never> {
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));
}
}