bizmatch-project/bizmatch/src/app/services/auth.service.ts

278 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<UserRole | null>(null);
public userRole$ = this.userRoleSubject.asObservable();
// Referenz für den gecachten API-Aufruf
private cachedUserRole$: Observable<UserRole | null> | 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<UserCredential> {
// 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<UserCredential> {
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<UserCredential> {
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<void> {
localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('photoURL');
this.clearRoleCache();
this.userRoleSubject.next(null);
return this.auth.signOut();
}
isAdmin(): Observable<boolean> {
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<UserRole | null> {
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<boolean> {
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<void> {
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<string | null> {
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<string | null> {
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();
}
}
}