// auth.service.ts import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { FirebaseApp } from '@angular/fire/app'; import { GoogleAuthProvider, UserCredential, createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'; import { BehaviorSubject, Observable, catchError, firstValueFrom, map, of, shareReplay, take, tap } from 'rxjs'; import { environment } from '../../environments/environment'; import { MailService } from './mail.service'; export type UserRole = 'admin' | 'pro' | 'guest'; @Injectable({ providedIn: 'root', }) export class AuthService { private app = inject(FirebaseApp); private auth = getAuth(this.app); private http = inject(HttpClient); private mailService = inject(MailService); // Add a BehaviorSubject to track the current user role private userRoleSubject = new BehaviorSubject(null); public userRole$ = this.userRoleSubject.asObservable(); // Referenz für den gecachten API-Aufruf private cachedUserRole$: Observable | null = null; // Zeitraum in ms, nach dem der Cache zurückgesetzt werden soll (z.B. 5 Minuten) private cacheDuration = 5 * 60 * 1000; private lastCacheTime = 0; constructor() { // Load role from token when service is initialized this.loadRoleFromToken(); } private loadRoleFromToken(): void { this.getToken().then(token => { if (token) { const role = this.extractRoleFromToken(token); this.userRoleSubject.next(role); } else { this.userRoleSubject.next(null); } }); } private extractRoleFromToken(token: string): UserRole | null { try { const payloadBase64 = token.split('.')[1]; const payloadJson = atob(payloadBase64.replace(/-/g, '+').replace(/_/g, '/')); const payload = JSON.parse(payloadJson); return (payload.role as UserRole) || null; } catch (e) { return null; } } // Registrierung mit Email und Passwort async registerWithEmail(email: string, password: string): Promise { // Bestimmen der aktuellen Umgebung/Domain für die Verifizierungs-URL let verificationUrl = ''; // Prüfen der aktuellen Umgebung basierend auf dem Host const currentHost = window.location.hostname; if (currentHost.includes('localhost')) { verificationUrl = 'http://localhost:4200/email-authorized'; } else if (currentHost.includes('dev.bizmatch.net')) { verificationUrl = 'https://dev.bizmatch.net/email-authorized'; } else { verificationUrl = 'https://www.bizmatch.net/email-authorized'; } // ActionCode-Einstellungen mit der dynamischen URL const actionCodeSettings = { url: `${verificationUrl}?email=${email}`, handleCodeInApp: true, }; // Benutzer erstellen const userCredential = await createUserWithEmailAndPassword(this.auth, email, password); // E-Mail-Verifizierung mit den angepassten ActionCode-Einstellungen senden if (userCredential.user) { //await sendEmailVerification(userCredential.user, actionCodeSettings); this.mailService.sendVerificationEmail(userCredential.user.email).subscribe({ next: () => { console.log('Verification email sent successfully'); // Erfolgsmeldung anzeigen }, error: error => { console.error('Error sending verification email', error); // Fehlermeldung anzeigen }, }); } // const token = await userCredential.user.getIdToken(); // localStorage.setItem('authToken', token); // localStorage.setItem('refreshToken', userCredential.user.refreshToken); // if (userCredential.user.photoURL) { // localStorage.setItem('photoURL', userCredential.user.photoURL); // } return userCredential; } // Login mit Email und Passwort loginWithEmail(email: string, password: string): Promise { return signInWithEmailAndPassword(this.auth, email, password).then(async userCredential => { if (userCredential.user) { const token = await userCredential.user.getIdToken(); localStorage.setItem('authToken', token); localStorage.setItem('refreshToken', userCredential.user.refreshToken); if (userCredential.user.photoURL) { localStorage.setItem('photoURL', userCredential.user.photoURL); } this.loadRoleFromToken(); } return userCredential; }); } // Login mit Google loginWithGoogle(): Promise { const provider = new GoogleAuthProvider(); return signInWithPopup(this.auth, provider).then(async userCredential => { if (userCredential.user) { const token = await userCredential.user.getIdToken(); localStorage.setItem('authToken', token); localStorage.setItem('refreshToken', userCredential.user.refreshToken); if (userCredential.user.photoURL) { localStorage.setItem('photoURL', userCredential.user.photoURL); } this.loadRoleFromToken(); } return userCredential; }); } // Logout: Token, RefreshToken und photoURL entfernen logout(): Promise { localStorage.removeItem('authToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('photoURL'); this.clearRoleCache(); this.userRoleSubject.next(null); return this.auth.signOut(); } isAdmin(): Observable { return this.getUserRole().pipe( map(role => role === 'admin'), // take(1) ist optional - es beendet die Subscription, nachdem ein Wert geliefert wurde // Nützlich, wenn du die Methode in einem Template mit dem async pipe verwendest take(1), ); } // Get current user's role from the server with caching getUserRole(): Observable { const now = Date.now(); // Cache zurücksetzen, wenn die Caching-Zeit abgelaufen ist oder kein Cache existiert if (!this.cachedUserRole$ || now - this.lastCacheTime > this.cacheDuration) { this.lastCacheTime = now; this.cachedUserRole$ = this.http.get<{ role: UserRole | null }>(`${environment.apiBaseUrl}/bizmatch/auth/me/role`).pipe( map(response => response.role), tap(role => this.userRoleSubject.next(role)), catchError(error => { console.error('Error fetching user role', error); return of(null); }), // Cache für mehrere Subscriber und behalte den letzten Wert // Der Parameter 1 gibt an, dass der letzte Wert gecacht werden soll // refCount: false bedeutet, dass der Cache nicht zurückgesetzt wird, wenn keine Subscriber mehr da sind shareReplay({ bufferSize: 1, refCount: false }), ); } return this.cachedUserRole$; } clearRoleCache(): void { this.cachedUserRole$ = null; this.lastCacheTime = 0; } // Check if user has a specific role hasRole(role: UserRole): Observable { return this.userRole$.pipe( map(userRole => { if (role === 'guest') { // Any authenticated user can access guest features return userRole !== null; } else if (role === 'pro') { // Both pro and admin can access pro features return userRole === 'pro' || userRole === 'admin'; } else if (role === 'admin') { // Only admin can access admin features return userRole === 'admin'; } return false; }), ); } // Force refresh the token to get updated custom claims async refreshUserClaims(): Promise { this.clearRoleCache(); if (this.auth.currentUser) { await this.auth.currentUser.getIdToken(true); const token = await this.auth.currentUser.getIdToken(); localStorage.setItem('authToken', token); this.loadRoleFromToken(); } } // Prüft, ob ein Token noch gültig ist (über die "exp"-Eigenschaft) private isTokenValid(token: string): boolean { try { const payloadBase64 = token.split('.')[1]; const payloadJson = atob(payloadBase64.replace(/-/g, '+').replace(/_/g, '/')); const payload = JSON.parse(payloadJson); const exp = payload.exp; const now = Math.floor(Date.now() / 1000); return exp > now; } catch (e) { return false; } } private isEMailVerified(token: string): boolean { try { const payloadBase64 = token.split('.')[1]; const payloadJson = atob(payloadBase64.replace(/-/g, '+').replace(/_/g, '/')); const payload = JSON.parse(payloadJson); return payload.email_verified; } catch (e) { return false; } } // Versucht, mit dem RefreshToken einen neuen Access Token zu erhalten async refreshToken(): Promise { const storedRefreshToken = localStorage.getItem('refreshToken'); if (!storedRefreshToken) { return null; } const apiKey = environment.firebaseConfig.apiKey; // Stelle sicher, dass dieser Wert in Deiner environment.ts gesetzt ist const url = `https://securetoken.googleapis.com/v1/token?key=${apiKey}`; const body = new HttpParams().set('grant_type', 'refresh_token').set('refresh_token', storedRefreshToken); const headers = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }); try { const response: any = await firstValueFrom(this.http.post(url, body.toString(), { headers })); // response enthält z. B. id_token, refresh_token, expires_in etc. const newToken = response.id_token; const newRefreshToken = response.refresh_token; localStorage.setItem('authToken', newToken); localStorage.setItem('refreshToken', newRefreshToken); return newToken; } catch (error) { console.error('Error refreshing token:', error); return null; } } /** * Gibt einen gültigen Token zurück. * Falls der gespeicherte Token noch gültig ist, wird er zurückgegeben. * Ansonsten wird versucht, einen neuen Token mit dem RefreshToken zu holen. * Ist auch das nicht möglich, wird null zurückgegeben. */ async getToken(): Promise { const token = localStorage.getItem('authToken'); if (token && !this.isEMailVerified(token)) { return null; } else if (token && this.isTokenValid(token) && this.isEMailVerified(token)) { return token; } else { return await this.refreshToken(); } } }