einbau von rollen, neue Admin Ansicht

This commit is contained in:
Andreas Knuth 2025-03-08 11:18:31 +01:00
parent dded8b8ca9
commit 5a56b3554d
29 changed files with 788 additions and 426 deletions

View File

@ -75,6 +75,7 @@
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"kysely-codegen": "^0.15.0", "kysely-codegen": "^0.15.0",
"nest-commander": "^3.16.1",
"pg-to-ts": "^4.1.1", "pg-to-ts": "^4.1.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",

View File

@ -19,12 +19,13 @@ import { ConfigModule } from '@nestjs/config';
import { APP_INTERCEPTOR } from '@nestjs/core'; import { APP_INTERCEPTOR } from '@nestjs/core';
import { ClsMiddleware, ClsModule } from 'nestjs-cls'; import { ClsMiddleware, ClsModule } from 'nestjs-cls';
import path from 'path'; import path from 'path';
import { AuthService } from './auth/auth.service';
import { FirebaseAdminModule } from './firebase-admin/firebase-admin.module';
import { LoggingInterceptor } from './interceptors/logging.interceptor'; import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { UserInterceptor } from './interceptors/user.interceptor'; import { UserInterceptor } from './interceptors/user.interceptor';
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware'; import { RequestDurationMiddleware } from './request-duration/request-duration.middleware';
import { SelectOptionsModule } from './select-options/select-options.module'; import { SelectOptionsModule } from './select-options/select-options.module';
import { UserModule } from './user/user.module'; import { UserModule } from './user/user.module';
import { FirebaseAdminModule } from './firebase-admin/firebase-admin.module';
//loadEnvFiles(); //loadEnvFiles();
console.log('Loaded environment variables:'); console.log('Loaded environment variables:');
@ -82,6 +83,7 @@ console.log('Loaded environment variables:');
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor, // Registriere den LoggingInterceptor global useClass: LoggingInterceptor, // Registriere den LoggingInterceptor global
}, },
AuthService,
], ],
}) })
export class AppModule implements NestModule { export class AppModule implements NestModule {

View File

@ -1,11 +1,17 @@
import { Body, Controller, HttpException, HttpStatus, Inject, Post } from '@nestjs/common'; import { Body, Controller, Get, HttpException, HttpStatus, Inject, Param, Post, Query, Req, UseGuards } from '@nestjs/common';
import * as admin from 'firebase-admin'; import * as admin from 'firebase-admin';
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { LocalhostGuard } from 'src/jwt-auth/localhost.guard';
import { UserRole, UsersResponse } from 'src/models/main.model';
import { AuthService } from './auth.service';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor( constructor(
@Inject('FIREBASE_ADMIN') @Inject('FIREBASE_ADMIN')
private readonly firebaseAdmin: typeof admin, private readonly firebaseAdmin: typeof admin,
private readonly authService: AuthService,
) {} ) {}
@Post('verify-email') @Post('verify-email')
async verifyEmail(@Body('oobCode') oobCode: string, @Body('email') email: string) { async verifyEmail(@Body('oobCode') oobCode: string, @Body('email') email: string) {
@ -33,5 +39,91 @@ export class AuthController {
throw new HttpException(error.message || 'Failed to verify email', HttpStatus.BAD_REQUEST); throw new HttpException(error.message || 'Failed to verify email', HttpStatus.BAD_REQUEST);
} }
} }
@Post(':uid/role')
@UseGuards(AuthGuard, AdminGuard) // Only admins can change roles
async setUserRole(@Param('uid') uid: string, @Body('role') role: UserRole): Promise<{ success: boolean }> {
await this.authService.setUserRole(uid, role);
return { success: true };
}
@Get('me/role')
@UseGuards(AuthGuard)
async getMyRole(@Req() req: any): Promise<{ role: UserRole | null }> {
console.log('->', req.user);
console.log('-->', req.user.uid);
const uid = req.user.uid; // From FirebaseAuthGuard
const role = await this.authService.getUserRole(uid);
return { role };
}
@Get(':uid/role')
@UseGuards(AuthGuard)
async getUserRole(@Param('uid') uid: string): Promise<{ role: UserRole | null }> {
const role = await this.authService.getUserRole(uid);
return { role };
}
@Get('role/:role')
@UseGuards(AuthGuard, AdminGuard) // Only admins can list users by role
async getUsersByRole(@Param('role') role: UserRole): Promise<{ users: any[] }> {
const users = await this.authService.getUsersByRole(role);
// Map to simpler objects to avoid circular references
const simplifiedUsers = users.map(user => ({
uid: user.uid,
email: user.email,
displayName: user.displayName,
}));
return { users: simplifiedUsers };
}
/**
* Ruft alle Firebase-Benutzer mit ihren Rollen ab
* @param maxResults Maximale Anzahl an zurückzugebenden Benutzern (optional, Standard: 1000)
* @param pageToken Token für die Paginierung (optional)
* @returns Eine Liste von Benutzern mit ihren Rollen und Metadaten
*/
@Get()
@UseGuards(AuthGuard, AdminGuard) // Only admins can list all users
async getAllUsers(@Query('maxResults') maxResults?: number, @Query('pageToken') pageToken?: string): Promise<UsersResponse> {
const result = await this.authService.getAllUsers(maxResults ? parseInt(maxResults.toString(), 10) : undefined, pageToken);
return {
users: result.users,
totalCount: result.users.length,
...(result.pageToken && { pageToken: result.pageToken }),
};
}
/**
* Endpoint zum direkten Einstellen einer Rolle für Debug-Zwecke
* WARNUNG: Dieser Endpoint sollte in der Produktion entfernt oder stark gesichert werden
*/
@Post('set-role')
@UseGuards(AuthGuard, LocalhostGuard)
async setUserRoleOnLocalhost(@Req() req: any, @Body('role') role: UserRole): Promise<{ success: boolean; message: string }> {
try {
const uid = req.user.uid;
// Aktuelle Rolle protokollieren
const currentUser = await this.authService.getUserRole(uid);
console.log(`Changing role for user ${uid} from ${currentUser} to ${role}`);
// Neue Rolle setzen
await this.authService.setUserRole(uid, role);
// Rolle erneut prüfen, um zu bestätigen
const newRole = await this.authService.getUserRole(uid);
return {
success: true,
message: `Rolle für Benutzer ${uid} von ${currentUser} zu ${newRole} geändert`,
};
} catch (error) {
console.error('Fehler beim Setzen der Rolle:', error);
return {
success: false,
message: `Fehler: ${error.message}`,
};
}
}
} }

View File

@ -2,11 +2,13 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module'; import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Module({ @Module({
imports: [ConfigModule.forRoot({ envFilePath: '.env' }),FirebaseAdminModule], imports: [ConfigModule.forRoot({ envFilePath: '.env' }),FirebaseAdminModule],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService],
exports: [], exports: [],
}) })
export class AuthModule {} export class AuthModule {}

View File

@ -0,0 +1,113 @@
import { Inject, Injectable } from '@nestjs/common';
import * as admin from 'firebase-admin';
import { FirebaseUserInfo, UserRole } from 'src/models/main.model';
@Injectable()
export class AuthService {
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
/**
* Set a user's role via Firebase custom claims
*/
async setUserRole(uid: string, role: UserRole): Promise<void> {
try {
// Get the current custom claims
const user = await this.firebaseAdmin.auth().getUser(uid);
const currentClaims = user.customClaims || {};
// Set the new role
await this.firebaseAdmin.auth().setCustomUserClaims(uid, {
...currentClaims,
role: role,
});
} catch (error) {
console.error('Error setting user role:', error);
throw error;
}
}
/**
* Get a user's current role
*/
async getUserRole(uid: string): Promise<UserRole | null> {
try {
const user = await this.firebaseAdmin.auth().getUser(uid);
const claims = user.customClaims || {};
return (claims.role as UserRole) || null;
} catch (error) {
console.error('Error getting user role:', error);
throw error;
}
}
/**
* Get all users with a specific role
*/
async getUsersByRole(role: UserRole): Promise<admin.auth.UserRecord[]> {
// Note: Firebase Admin doesn't provide a direct way to query users by custom claims
// For a production app, you might want to store role information in Firestore as well
// This is a simple implementation that lists all users and filters them
try {
const listUsersResult = await this.firebaseAdmin.auth().listUsers();
return listUsersResult.users.filter(user => user.customClaims && user.customClaims.role === role);
} catch (error) {
console.error('Error getting users by role:', error);
throw error;
}
}
/**
* Get all Firebase users with their roles
* @param maxResults Maximum number of users to return (optional, default 1000)
* @param pageToken Token for pagination (optional)
*/
async getAllUsers(maxResults: number = 1000, pageToken?: string): Promise<{ users: FirebaseUserInfo[]; pageToken?: string }> {
try {
const listUsersResult = await this.firebaseAdmin.auth().listUsers(maxResults, pageToken);
const users = listUsersResult.users.map(user => this.mapUserRecord(user));
return {
users,
pageToken: listUsersResult.pageToken,
};
} catch (error) {
console.error('Error getting all users:', error);
throw error;
}
}
/**
* Maps a Firebase UserRecord to our FirebaseUserInfo interface
*/
private mapUserRecord(user: admin.auth.UserRecord): FirebaseUserInfo {
return {
uid: user.uid,
email: user.email || null,
displayName: user.displayName || null,
photoURL: user.photoURL || null,
phoneNumber: user.phoneNumber || null,
disabled: user.disabled,
emailVerified: user.emailVerified,
role: user.customClaims?.role || null,
creationTime: user.metadata.creationTime,
lastSignInTime: user.metadata.lastSignInTime,
// Optionally include other customClaims if needed
customClaims: user.customClaims,
};
}
/**
* Set default role for a new user
*/
async setDefaultRole(uid: string): Promise<void> {
return this.setUserRole(uid, 'guest');
}
/**
* Verify if a user has a specific role
*/
async hasRole(uid: string, role: UserRole): Promise<boolean> {
const userRole = await this.getUserRole(uid);
return userRole === role;
}
}

View File

@ -0,0 +1,20 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// The FirebaseAuthGuard should run before this guard
// and populate the request.user object
if (!request.user) {
throw new ForbiddenException('User not authenticated');
}
if (request.user.role !== 'admin') {
throw new ForbiddenException('Requires admin privileges');
}
return true;
}
}

View File

@ -4,53 +4,39 @@ import * as admin from 'firebase-admin';
@Injectable() @Injectable()
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor( constructor(
@Inject('FIREBASE_ADMIN') @Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App,
private readonly firebaseAdmin: typeof admin,
) {} ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>(); const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request); const authHeader = request.headers.authorization;
if (!token) { if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('No token provided'); throw new UnauthorizedException('Missing or invalid authorization token');
} }
const token = authHeader.split('Bearer ')[1];
try { try {
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token); const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
request['user'] = decodedToken;
// Check if email is verified (optional but recommended)
if (!decodedToken.email_verified) {
throw new UnauthorizedException('Email not verified');
}
// Add the user to the request
request.user = {
uid: decodedToken.uid,
email: decodedToken.email,
role: decodedToken.role || null,
// Add other user info as needed
};
return true; return true;
} catch (error) { } catch (error) {
throw new UnauthorizedException('Invalid token'); throw new UnauthorizedException('Invalid token');
} }
} }
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
} }
// @Injectable()
// export class AuthGuard implements CanActivate {
// async canActivate(context: ExecutionContext): Promise<boolean> {
// const request = context.switchToHttp().getRequest<Request>();
// const token = this.extractTokenFromHeader(request);
// if (!token) {
// throw new UnauthorizedException('No token provided');
// }
// try {
// const decodedToken = await admin.auth().verifyIdToken(token);
// request['user'] = decodedToken; // Fügen Sie die Benutzerdaten dem Request-Objekt hinzu
// return true;
// } catch (error) {
// throw new UnauthorizedException('Invalid token');
// }
// }
// private extractTokenFromHeader(request: Request): string | undefined {
// const [type, token] = request.headers['authorization']?.split(' ') ?? [];
// return type === 'Bearer' ? token : undefined;
// }
// }

View File

@ -1,16 +0,0 @@
// import * as admin from 'firebase-admin';
// import { ServiceAccount } from 'firebase-admin';
// console.log('--> '+process.env['FIREBASE_PROJECT_ID'])
// const serviceAccount: ServiceAccount = {
// projectId: process.env['FIREBASE_PROJECT_ID'],
// clientEmail: process.env['FIREBASE_CLIENT_EMAIL'],
// privateKey: process.env['FIREBASE_PRIVATE_KEY']?.replace(/\\n/g, '\n'), // Ersetzen Sie escaped newlines
// };
// if (!admin.apps.length) {
// admin.initializeApp({
// credential: admin.credential.cert(serviceAccount),
// });
// }
// export default admin;

View File

@ -0,0 +1,21 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class LocalhostGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const ip = request.ip;
// Liste der erlaubten IPs
const allowedIPs = ['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1'];
if (!allowedIPs.includes(ip)) {
console.warn(`Versuchter Zugriff von unerlaubter IP: ${ip}`);
throw new ForbiddenException('Dieser Endpunkt kann nur lokal aufgerufen werden');
}
return true;
}
}

View File

@ -3,54 +3,70 @@ import * as admin from 'firebase-admin';
@Injectable() @Injectable()
export class OptionalAuthGuard implements CanActivate { export class OptionalAuthGuard implements CanActivate {
constructor( constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
@Inject('FIREBASE_ADMIN')
private readonly firebaseAdmin: typeof admin,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>(); const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request); const authHeader = request.headers.authorization;
if (!token) { if (!authHeader || !authHeader.startsWith('Bearer ')) {
//throw new UnauthorizedException('Missing or invalid authorization token');
return true; return true;
} }
const token = authHeader.split('Bearer ')[1];
try { try {
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token); const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
request['user'] = decodedToken;
// Check if email is verified (optional but recommended)
if (!decodedToken.email_verified) {
//throw new UnauthorizedException('Email not verified');
return true;
}
// Add the user to the request
request.user = {
uid: decodedToken.uid,
email: decodedToken.email,
role: decodedToken.role || null,
// Add other user info as needed
};
return true; return true;
} catch (error) { } catch (error) {
//throw new UnauthorizedException('Invalid token'); //throw new UnauthorizedException('Invalid token');
request['user'] = null;
return true; return true;
} }
} }
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
} }
// import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
// import * as admin from 'firebase-admin';
// @Injectable() // @Injectable()
// export class OptionalAuthGuard implements CanActivate { // export class OptionalAuthGuard implements CanActivate {
// constructor(
// @Inject('FIREBASE_ADMIN')
// private readonly firebaseAdmin: typeof admin,
// ) {}
// async canActivate(context: ExecutionContext): Promise<boolean> { // async canActivate(context: ExecutionContext): Promise<boolean> {
// const request = context.switchToHttp().getRequest<Request>(); // const request = context.switchToHttp().getRequest<Request>();
// const token = this.extractTokenFromHeader(request); // const token = this.extractTokenFromHeader(request);
// if (!token) { // if (!token) {
// return true; // Kein Token vorhanden, aber Zugriff erlaubt // return true;
// } // }
// try { // try {
// const decodedToken = await admin.auth().verifyIdToken(token); // const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
// request['user'] = decodedToken; // Benutzerdaten zum Request hinzufügen, wenn Token gültig // request['user'] = decodedToken;
// return true;
// } catch (error) { // } catch (error) {
// // Bei ungültigem Token wird kein Fehler geworfen, sondern einfach kein User gesetzt // //throw new UnauthorizedException('Invalid token');
// request['user'] = null; // request['user'] = null;
// return true;
// } // }
// return true; // Zugriff wird immer erlaubt
// } // }
// private extractTokenFromHeader(request: Request): string | undefined { // private extractTokenFromHeader(request: Request): string | undefined {

View File

@ -278,6 +278,26 @@ export interface Checkout {
email: string; email: string;
name: string; name: string;
} }
export type UserRole = 'admin' | 'pro' | 'guest' | null;
export interface FirebaseUserInfo {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
phoneNumber: string | null;
disabled: boolean;
emailVerified: boolean;
role: UserRole;
creationTime?: string;
lastSignInTime?: string;
customClaims?: Record<string, any>;
}
export interface UsersResponse {
users: FirebaseUserInfo[];
totalCount: number;
pageToken?: string;
}
export function isEmpty(value: any): boolean { export function isEmpty(value: any): boolean {
// Check for undefined or null // Check for undefined or null
if (value === undefined || value === null) { if (value === undefined || value === null) {

View File

@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { AuthService } from './auth/auth.service';
@Injectable()
@Command({ name: 'setup-admin', description: 'Set up the first admin user' })
export class SetupAdminCommand extends CommandRunner {
constructor(private readonly authService: AuthService) {
super();
}
async run(passedParams: string[]): Promise<void> {
if (passedParams.length < 1) {
console.error('Please provide a user UID');
return;
}
const uid = passedParams[0];
try {
await this.authService.setUserRole(uid, 'admin');
console.log(`User ${uid} has been set as admin`);
} catch (error) {
console.error('Error setting admin role:', error);
}
}
}

View File

@ -4,6 +4,7 @@ import { Logger } from 'winston';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service';
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
import { AuthGuard } from 'src/jwt-auth/auth.guard'; import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard'; import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { User } from '../models/db.model'; import { User } from '../models/db.model';
@ -31,11 +32,11 @@ export class UserController {
const user = await this.userService.getUserById(id); const user = await this.userService.getUserById(id);
return user; return user;
} }
// @UseGuards(AdminAuthGuard) @UseGuards(AdminGuard)
// @Get('user/all') @Get('user/all')
// async getAllUser(): Promise<User[]> { async getAllUser(): Promise<User[]> {
// return await this.userService.getAllUser(); return await this.userService.getAllUser();
// } }
@UseGuards(OptionalAuthGuard) @UseGuards(OptionalAuthGuard)
@Post() @Post()

View File

@ -68,7 +68,7 @@
<li> <li>
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a> <a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
</li> </li>
@if(user.customerType==='professional' || user.customerType==='seller' || isAdmin()){ @if(user.customerType==='professional' || user.customerType==='seller' || (authService.isAdmin() | async)){
<li> <li>
@if(user.customerSubType==='broker'){ @if(user.customerSubType==='broker'){
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white" <a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
@ -94,7 +94,7 @@
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a> <a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
</li> </li>
</ul> </ul>
@if(isAdmin()){ @if(authService.isAdmin() | async){
<ul class="py-2"> <ul class="py-2">
<li> <li>
<a routerLink="admin/users" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Users (Admin)</a> <a routerLink="admin/users" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Users (Admin)</a>
@ -121,8 +121,7 @@
>Properties</a >Properties</a
> >
</li> </li>
} } @if ((numberOfBroker$ | async) > 0) {
@if ((numberOfBroker$ | async) > 0) {
<li> <li>
<a <a
routerLink="/brokerListings" routerLink="/brokerListings"
@ -165,8 +164,7 @@
>Properties</a >Properties</a
> >
</li> </li>
} } @if ((numberOfBroker$ | async) > 0) {
@if ((numberOfBroker$ | async) > 0) {
<li> <li>
<a <a
routerLink="/brokerListings" routerLink="/brokerListings"
@ -219,8 +217,7 @@
>Properties</a >Properties</a
> >
</li> </li>
} } @if ((numberOfBroker$ | async) > 0) {
@if ((numberOfBroker$ | async) > 0) {
<li> <li>
<a <a
routerLinkActive="active-link" routerLinkActive="active-link"

View File

@ -17,7 +17,7 @@ import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { SharedService } from '../../services/shared.service'; import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, isAdmin, map2User } from '../../utils/utils'; import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component'; import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service'; import { ModalService } from '../search-modal/modal.service';
@UntilDestroy() @UntilDestroy()
@ -58,7 +58,7 @@ export class HeaderComponent {
private searchService: SearchService, private searchService: SearchService,
private criteriaChangeService: CriteriaChangeService, private criteriaChangeService: CriteriaChangeService,
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private authService: AuthService, public authService: AuthService,
private listingService: ListingsService, private listingService: ListingsService,
) {} ) {}
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
@ -198,7 +198,4 @@ export class HeaderComponent {
toggleSortDropdown() { toggleSortDropdown() {
this.sortDropdownVisible = !this.sortDropdownVisible; this.sortDropdownVisible = !this.sortDropdownVisible;
} }
isAdmin() {
return isAdmin(this.user.email);
}
} }

View File

@ -1,156 +1,154 @@
<!-- src/app/components/user-list/user-list.component.html --> <div class="container mx-auto px-4 py-8 max-w-7xl">
<div class="container mx-auto p-4"> <h2 class="text-2xl font-bold text-gray-800 mb-6">Benutzerverwaltung</h2>
<h1 class="text-2xl font-bold mb-4">Benutzerverwaltung</h1>
<!-- Ladeanzeige --> <!-- Rollenfilter -->
<div *ngIf="isLoading" class="flex justify-center"> <div class="mb-6">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div> <label for="roleFilter" class="block text-sm font-medium text-gray-700 mb-1">Nach Rolle filtern:</label>
<select id="roleFilter" class="block w-full md:w-64 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" [(ngModel)]="selectedRole" (change)="onRoleFilterChange(selectedRole)">
<option value="all">Alle Benutzer</option>
<option value="admin">Admin</option>
<option value="pro">Pro</option>
<option value="guest">Guest</option>
<option [ngValue]="null">Keine Rolle</option>
</select>
</div> </div>
<!-- Fehlermeldung --> <!-- Fehlermeldung -->
<div *ngIf="error" class="text-red-500 mb-4"> <div *ngIf="error" class="bg-red-50 border border-red-200 text-red-800 rounded-md p-4 mb-6 relative">
{{ error }} <span class="block sm:inline">{{ error }}</span>
</div> <button type="button" class="absolute top-4 right-4" (click)="error = null">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<!-- Benutzer-Tabelle --> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<table *ngIf="!isLoading && !error" class="min-w-full bg-white border">
<thead>
<tr>
<!-- <th class="py-2 px-4 border-b">ID</th> -->
<th class="py-2 px-4 border-b">Vorname</th>
<th class="py-2 px-4 border-b">Nachname</th>
<th class="py-2 px-4 border-b">E-Mail</th>
<th class="py-2 px-4 border-b">DB</th>
<th class="py-2 px-4 border-b">Keycloak</th>
<th class="py-2 px-4 border-b">Stripe</th>
<th class="py-2 px-4 border-b">Sub</th>
<th class="py-2 px-4 border-b">Aktionen</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of combinedUsers; let i = index" class="text-center">
<td class="py-2 px-4 border-b">
{{ user.appUser?.firstname || user.keycloakUser?.firstName || user.stripeUser?.name || '—' }}
</td>
<td class="py-2 px-4 border-b">
{{ user.appUser?.lastname || user.keycloakUser?.lastName || '—' }}
</td>
<td class="py-2 px-4 border-b">
{{ user.appUser?.email || user.keycloakUser?.email || user.stripeUser?.email }}
</td>
<td class="py-2 px-4 border-b">
<input type="checkbox" [checked]="!!user.appUser" disabled />
</td>
<td class="py-2 px-4 border-b">
<input type="checkbox" [checked]="!!user.keycloakUser" disabled />
</td>
<td class="py-2 px-4 border-b">
<input type="checkbox" [checked]="!!user.stripeUser" disabled />
</td>
<td class="py-2 px-4 border-b">
@if(!!user.stripeSubscription){
<input type="checkbox" [checked]="!!user.stripeSubscription" disabled attr.data-tooltip-target="tooltip-{{ i }}" />
}@else {
<input type="checkbox" [checked]="!!user.stripeSubscription" disabled />
} @if(!!user.stripeSubscription){
<app-tooltip id="tooltip-{{ i }}" [text]="getSubscriptionInfo(user.stripeSubscription)"></app-tooltip>
}
</td>
<td class="py-2 px-4 border-b space-x-2">
<button class="share share-delete text-white font-bold text-xs py-1 px-2 inline-flex items-center" attr.data-dropdown-toggle="dropdown_{{ user.appUser?.id }}">
Delete<svg class="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4" />
</svg> </svg>
</button> </button>
<!-- Dropdown menu -->
<div id="dropdown_{{ user.appUser?.id }}" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton">
<li>
<a class="block px-4 py-2 hover:bg-gray-100" (click)="delete(user)">Complete</a>
</li>
@if(user.stripeSubscription){
<li>
<a class="block px-4 py-2 hover:bg-gray-100" (click)="deleteFromStripe(user)">From Stripe</a>
</li>
}
</ul>
</div> </div>
<button class="share share-cc text-white font-bold text-xs py-1 px-2 inline-flex items-center" (click)="showCreditCardInfo(user)" [disabled]="!user.stripeSubscription">
<i class="fa-solid fa-credit-card"></i>&nbsp;CC Info
</button>
<button class="share share-msg text-white font-bold text-xs py-1 px-2 inline-flex items-center" (click)="showMessages(user)"><i class="fa-solid fa-message"></i>&nbsp;Messages</button>
</td>
</tr>
</tbody>
</table>
<!-- Flowbite Modal für Kreditkarteninformationen --> <!-- Ladeanzeige -->
<div *ngIf="showModal" class="fixed top-0 left-0 right-0 z-50 flex items-center justify-center w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full" aria-modal="true" role="dialog"> <div *ngIf="loading" class="flex justify-center my-8">
<div class="relative w-full max-w-2xl max-h-full"> <svg class="animate-spin h-8 w-8 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<!-- Modal-Content --> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700"> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<!-- Modal-Kopf --> </svg>
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"> </div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Kreditkarteninformationen</h3>
<button <!-- Benutzertabelle -->
type="button" <div class="overflow-x-auto shadow-md rounded-lg bg-white">
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" <table class="min-w-full divide-y divide-gray-200">
(click)="closeModal()" <thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">E-Mail</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rolle</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">E-Mail bestätigt</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Letzter Login</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr *ngFor="let user of users" class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10">
<img *ngIf="user.photoURL" [src]="user.photoURL" alt="Profilbild" class="h-10 w-10 rounded-full" />
<div *ngIf="!user.photoURL" class="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
<span class="text-gray-500 text-sm">{{ (user.displayName || user.email || '?').charAt(0).toUpperCase() }}</span>
</div>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">{{ user.displayName || 'Kein Name' }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">{{ user.email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
[ngClass]="{
'bg-red-100 text-red-800': user.role === 'admin',
'bg-yellow-100 text-yellow-800': user.role === 'pro',
'bg-blue-100 text-blue-800': user.role === 'guest',
'bg-gray-100 text-gray-800': user.role === null
}"
> >
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> {{ user.role || 'Keine' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div *ngIf="user.emailVerified" class="flex-shrink-0 h-5 w-5 text-green-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div *ngIf="!user.emailVerified" class="flex-shrink-0 h-5 w-5 text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd" clip-rule="evenodd"
></path> />
</svg>
</div>
<div class="ml-2 text-sm text-gray-500">
{{ user.emailVerified ? 'Ja' : 'Nein' }}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ user.lastSignInTime | date : 'medium' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<div class="relative" #dropdown>
<button (click)="dropdown.classList.toggle('active')" class="text-indigo-600 hover:text-indigo-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-50 rounded-md px-3 py-1 text-sm">
Rolle ändern
<svg class="h-4 w-4 inline-block ml-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
<div *ngIf="dropdown.classList.contains('active')" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
<div class="py-1" role="menu" aria-orientation="vertical">
<a (click)="changeUserRole(user, 'admin'); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Admin</a>
<a (click)="changeUserRole(user, 'pro'); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Pro</a>
<a (click)="changeUserRole(user, 'guest'); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Guest</a>
<div class="border-t border-gray-100"></div>
<a (click)="changeUserRole(user, null); dropdown.classList.remove('active')" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer">Keine Rolle</a>
</div> </div>
<!-- Modal-Körper -->
<div class="p-6 space-y-6">
<div *ngIf="ccInfoLoading" class="flex justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div> </div>
<div *ngIf="ccInfoError" class="text-red-500">
{{ ccInfoError }}
</div> </div>
</td>
<div *ngIf="!ccInfoLoading && !ccInfoError">
<ng-container *ngIf="creditCardInfo.length > 0; else noCCInfo">
<table class="min-w-full bg-white border">
<thead>
<tr>
<th class="py-2 px-4 border-b">Kartenmarke</th>
<th class="py-2 px-4 border-b">Letzte 4</th>
<th class="py-2 px-4 border-b">Ablaufdatum</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let method of creditCardInfo" class="text-center">
<td class="py-2 px-4 border-b">{{ method.card?.brand || '—' }}</td>
<td class="py-2 px-4 border-b">{{ method.card?.last4 || '—' }}</td>
<td class="py-2 px-4 border-b">{{ method.card?.exp_month }}/{{ method.card?.exp_year }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</ng-container> </div>
<ng-template #noCCInfo>
<p>Keine Kreditkarteninformationen verfügbar.</p> <!-- Keine Benutzer gefunden -->
</ng-template> <div *ngIf="users.length === 0 && !loading" class="bg-blue-50 border border-blue-200 text-blue-800 rounded-md p-4 my-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm">Keine Benutzer gefunden.</p>
</div> </div>
</div> </div>
<!-- Modal-Fuß --> </div>
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
<!-- "Mehr laden"-Button -->
<div *ngIf="hasMoreUsers" class="flex justify-center mt-6 mb-8">
<button <button
type="button" (click)="loadMoreUsers()"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" [disabled]="loading"
(click)="closeModal()" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
> >
Schließen <svg *ngIf="loading" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ loading ? 'Lädt...' : 'Weitere Benutzer laden' }}
</button> </button>
</div> </div>
</div>
</div>
</div>
</div> </div>

View File

@ -1,138 +1,97 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule, DatePipe } from '@angular/common'; import { FirebaseUserInfo, UserRole, UsersResponse } from '../../../../../../bizmatch-server/src/models/main.model';
import { PaymentMethod } from '@stripe/stripe-js';
import { initFlowbite } from 'flowbite';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { CombinedUser, StripeSubscription } from '../../../../../../bizmatch-server/src/models/main.model';
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
import { MessageService } from '../../../components/message/message.service';
import { TooltipComponent } from '../../../components/tooltip/tooltip.component';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
@Component({ @Component({
selector: 'app-user-list', selector: 'app-user-list',
standalone: true,
imports: [CommonModule, TooltipComponent],
providers: [DatePipe],
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrl: './user-list.component.scss', styleUrls: ['./user-list.component.scss'],
imports: [CommonModule, FormsModule],
standalone: true,
}) })
export class UserListComponent implements OnInit { export class UserListComponent implements OnInit {
combinedUsers: CombinedUser[] = []; users: FirebaseUserInfo[] = [];
isLoading = true; loading = false;
error: string | null = null; error: string | null = null;
selectedUser: CombinedUser | null = null;
creditCardInfo: PaymentMethod[] = []; // Paginierung
ccInfoLoading = false; pageToken?: string;
ccInfoError: string | null = null; hasMoreUsers = false;
showModal = false; maxResultsPerPage = 50;
constructor(private userService: UserService, private datePipe: DatePipe, private confirmationService: ConfirmationService, private messageService: MessageService) {}
// Filterung
selectedRole: UserRole | 'all' = 'all';
constructor(private userService: UserService) {}
ngOnInit(): void { ngOnInit(): void {
this.loadUsers(); this.loadUsers();
} }
ngAfterViewInit() {
// initFlowbite();
}
loadUsers(): void { loadUsers(): void {
this.userService.loadUsers().subscribe({ this.loading = true;
next: users => { this.error = null;
this.combinedUsers = users;
this.isLoading = false; if (this.selectedRole !== 'all') {
setTimeout(() => { // Benutzer nach Rolle filtern
initFlowbite(); this.userService.getUsersByRole(this.selectedRole).subscribe({
}, 10); next: response => {
this.users = response.users;
this.loading = false;
this.hasMoreUsers = false; // Bei Rollenfilterung keine Paginierung
}, },
error: err => { error: err => {
this.error = 'Fehler beim Laden der Benutzer'; this.error = 'Fehler beim Laden der Benutzer: ' + (err.message || err);
this.isLoading = false; this.loading = false;
console.error(err);
},
});
}
getSubscriptionInfo(subscription: StripeSubscription) {
return `${subscription.metadata['plan']} / ${subscription.status} / ${this.datePipe.transform(new Date(subscription.start_date * 1000))} / ${this.datePipe.transform(
new Date(subscription.current_period_end * 1000),
)}`;
}
async deleteFromStripe(user: CombinedUser) {
const confirmed = await this.confirmationService.showConfirmation({ message: `Do you want to delete the User from Stripe ?` });
if (confirmed) {
if (!user || !user.stripeUser) {
// Benutzer oder StripeUser nicht definiert
return;
}
const customerId = user.stripeUser.id; // Angenommen, 'id' ist die Kunden-ID
try {
// 1. Stripe User löschen
await this.userService.deleteCustomerFromStripe(customerId);
console.log('Stripe User erfolgreich gelöscht.');
// 2. App-User aktualisieren
const appUser = user.appUser;
if (appUser) {
const updatedUser: User = {
...appUser,
subscriptionId: null,
customerType: 'buyer',
subscriptionPlan: 'free',
customerSubType: null,
};
const savedUser = await this.userService.saveGuaranteed(updatedUser);
console.log('App-User erfolgreich aktualisiert:', savedUser);
}
this.messageService.addMessage({
severity: 'success',
text: 'Stripe User deleted.',
duration: 3000, // 3 seconds
});
// Optional: Aktualisieren Sie die Benutzerliste oder führen Sie andere Aktionen aus
} catch (error) {
console.error('Fehler beim Löschen des Benutzers:', error);
this.messageService.addMessage({
severity: 'danger',
text: 'Error is occured during the deletion of the user ...',
duration: 3000, // 3 seconds
});
}
}
}
delete(user: CombinedUser): void {}
showCreditCardInfo(user: CombinedUser): void {
this.selectedUser = user;
this.creditCardInfo = [];
this.ccInfoError = null;
this.ccInfoLoading = true;
this.showModal = true;
const email = user.appUser?.email || user.keycloakUser?.email || user.stripeUser?.email;
if (email) {
this.userService.getPaymentMethods(email).subscribe({
next: methods => {
this.creditCardInfo = methods;
this.ccInfoLoading = false;
},
error: err => {
this.ccInfoError = 'Fehler beim Laden der Kreditkarteninformationen';
this.ccInfoLoading = false;
console.error(err);
}, },
}); });
} else { } else {
this.ccInfoError = 'Keine gültige E-Mail-Adresse gefunden'; // Alle Benutzer mit Paginierung laden
this.ccInfoLoading = false; this.userService.getAllUsers(this.maxResultsPerPage, this.pageToken).subscribe({
next: (response: UsersResponse) => {
this.users = this.pageToken
? [...this.users, ...response.users] // Anhängen bei Paginierung
: response.users; // Ersetzen beim ersten Laden
this.pageToken = response.pageToken;
this.hasMoreUsers = !!response.pageToken;
this.loading = false;
},
error: err => {
this.error = 'Fehler beim Laden der Benutzer: ' + (err.message || err);
this.loading = false;
},
});
} }
} }
showMessages(user: CombinedUser): void {} loadMoreUsers(): void {
closeModal(): void { if (this.hasMoreUsers && !this.loading) {
this.showModal = false; this.loadUsers();
this.selectedUser = null; }
this.creditCardInfo = []; }
this.ccInfoError = null;
onRoleFilterChange(role: UserRole | 'all'): void {
this.selectedRole = role;
this.users = []; // Liste zurücksetzen
this.pageToken = undefined; // Paginierung zurücksetzen
this.loadUsers();
}
changeUserRole(user: FirebaseUserInfo, newRole: UserRole): void {
this.userService.setUserRole(user.uid, newRole).subscribe({
next: () => {
// Benutzer in der lokalen Liste aktualisieren
const index = this.users.findIndex(u => u.uid === user.uid);
if (index !== -1) {
this.users[index] = { ...user, role: newRole };
}
},
error: err => {
this.error = `Fehler beim Ändern der Rolle für ${user.email}: ${err.message || err}`;
},
});
} }
} }

View File

@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<div class="py-4 print:hidden"> <div class="py-4 print:hidden">
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){ @if(listing && listingUser && (listingUser?.email===user?.email || (authService.isAdmin() | async))){
<div class="inline"> <div class="inline">
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editBusinessListing', listing.id]"> <button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editBusinessListing', listing.id]">
<i class="fa-regular fa-pen-to-square"></i> <i class="fa-regular fa-pen-to-square"></i>

View File

@ -21,7 +21,7 @@ import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { createMailInfo, isAdmin, map2User } from '../../../utils/utils'; import { createMailInfo, map2User } from '../../../utils/utils';
// Import für Leaflet // Import für Leaflet
// Benannte Importe für Leaflet // Benannte Importe für Leaflet
import { AuthService } from '../../../services/auth.service'; import { AuthService } from '../../../services/auth.service';
@ -79,7 +79,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
private auditService: AuditService, private auditService: AuditService,
public emailService: EMailService, public emailService: EMailService,
private geoService: GeoService, private geoService: GeoService,
private authService: AuthService, public authService: AuthService,
) { ) {
super(); super();
this.router.events.subscribe(event => { this.router.events.subscribe(event => {
@ -115,9 +115,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
ngOnDestroy() { ngOnDestroy() {
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
} }
isAdmin() {
return isAdmin(this.keycloakUser?.email); //this.keycloakService.getUserRoles(true).includes('ADMIN');
}
async mail() { async mail() {
try { try {
this.mailinfo.email = this.listingUser.email; this.mailinfo.email = this.listingUser.email;

View File

@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<div class="py-4 print:hidden"> <div class="py-4 print:hidden">
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){ @if(listing && listingUser && (listingUser?.email===user?.email || (authService.isAdmin() | async))){
<div class="inline"> <div class="inline">
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editCommercialPropertyListing', listing.id]"> <button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editCommercialPropertyListing', listing.id]">
<i class="fa-regular fa-pen-to-square"></i> <i class="fa-regular fa-pen-to-square"></i>

View File

@ -24,7 +24,7 @@ import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { createMailInfo, isAdmin, map2User } from '../../../utils/utils'; import { createMailInfo, map2User } from '../../../utils/utils';
import { BaseDetailsComponent } from '../base-details.component'; import { BaseDetailsComponent } from '../base-details.component';
@Component({ @Component({
@ -83,7 +83,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
private messageService: MessageService, private messageService: MessageService,
private auditService: AuditService, private auditService: AuditService,
private emailService: EMailService, private emailService: EMailService,
private authService: AuthService, public authService: AuthService,
) { ) {
super(); super();
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl }; this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
@ -139,9 +139,6 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
.catch(error => console.error('Error initializing Flowbite:', error)); .catch(error => console.error('Error initializing Flowbite:', error));
}); });
} }
isAdmin() {
return isAdmin(this.keycloakUser?.email);
}
async mail() { async mail() {
try { try {
this.mailinfo.email = this.listingUser.email; this.mailinfo.email = this.listingUser.email;

View File

@ -137,7 +137,7 @@
</div> </div>
} }
</div> </div>
} @if( user?.email===keycloakUser?.email || isAdmin()){ } @if( user?.email===keycloakUser?.email || (authService.isAdmin() | async)){
<button class="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" [routerLink]="['/account', user.id]">Edit</button> <button class="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" [routerLink]="['/account', user.id]">Edit</button>
} }
</div> </div>

View File

@ -12,7 +12,7 @@ import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { formatPhoneNumber, isAdmin, map2User } from '../../../utils/utils'; import { formatPhoneNumber, map2User } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-details-user', selector: 'app-details-user',
@ -45,7 +45,7 @@ export class DetailsUserComponent {
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private imageService: ImageService, private imageService: ImageService,
public historyService: HistoryService, public historyService: HistoryService,
private authService: AuthService, public authService: AuthService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@ -59,8 +59,4 @@ export class DetailsUserComponent {
this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview ? this.user.companyOverview : ''); this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview ? this.user.companyOverview : '');
this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices ? this.user.offeredServices : ''); this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices ? this.user.offeredServices : '');
} }
isAdmin() {
return isAdmin(this.user.email);
}
} }

View File

@ -9,7 +9,7 @@
<input type="email" id="email" name="email" [(ngModel)]="user.email" disabled class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" /> <input type="email" id="email" name="email" [(ngModel)]="user.email" disabled class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<p class="text-xs text-gray-500 mt-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p> <p class="text-xs text-gray-500 mt-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p>
</div> </div>
@if (isProfessional || isAdmin()){ @if (isProfessional || (authService.isAdmin() | async)){
<div class="flex flex-row items-center justify-around md:space-x-4"> <div class="flex flex-row items-center justify-around md:space-x-4">
<div class="flex h-full justify-between flex-col"> <div class="flex h-full justify-between flex-col">
<p class="text-sm font-medium text-gray-700 mb-1">Company Logo</p> <p class="text-sm font-medium text-gray-700 mb-1">Company Logo</p>
@ -71,7 +71,7 @@
<option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option> <option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option>
</select> </select>
</div> --> </div> -->
@if (isAdmin() && !id){ @if ((authService.isAdmin() | async) && !id){
<div> <div>
<label for="customerType" class="block text-sm font-medium text-gray-700">User Type</label> <label for="customerType" class="block text-sm font-medium text-gray-700">User Type</label>
<span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">ADMIN</span> <span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">ADMIN</span>

View File

@ -33,7 +33,7 @@ import { SelectOptionsService } from '../../../services/select-options.service';
import { SharedService } from '../../../services/shared.service'; import { SharedService } from '../../../services/shared.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { isAdmin, map2User } from '../../../utils/utils'; import { map2User } from '../../../utils/utils';
import { TOOLBAR_OPTIONS } from '../../utils/defaults'; import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({ @Component({
selector: 'app-account', selector: 'app-account',
@ -96,7 +96,7 @@ export class AccountComponent {
// private subscriptionService: SubscriptionsService, // private subscriptionService: SubscriptionsService,
private datePipe: DatePipe, private datePipe: DatePipe,
private router: Router, private router: Router,
private authService: AuthService, public authService: AuthService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
setTimeout(() => { setTimeout(() => {
@ -264,9 +264,7 @@ export class AccountComponent {
const message = this.validationMessages.find(msg => msg.field === fieldName); const message = this.validationMessages.find(msg => msg.field === fieldName);
return message ? message.message : ''; return message ? message.message : '';
} }
isAdmin() {
return isAdmin(this.user.email);
}
setState(index: number, state: string) { setState(index: number, state: string) {
if (state === null) { if (state === null) {
this.user.areasServed[index].county = null; this.user.areasServed[index].county = null;

View File

@ -3,10 +3,12 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { FirebaseApp } from '@angular/fire/app'; import { FirebaseApp } from '@angular/fire/app';
import { GoogleAuthProvider, UserCredential, createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'; import { GoogleAuthProvider, UserCredential, createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth';
import { firstValueFrom } from 'rxjs'; import { BehaviorSubject, Observable, catchError, firstValueFrom, map, of, shareReplay, take, tap } from 'rxjs';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { MailService } from './mail.service'; import { MailService } from './mail.service';
export type UserRole = 'admin' | 'pro' | 'guest';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -15,9 +17,43 @@ export class AuthService {
private auth = getAuth(this.app); private auth = getAuth(this.app);
private http = inject(HttpClient); private http = inject(HttpClient);
private mailService = inject(MailService); 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;
// Registrierung mit Email und Passwort // Zeitraum in ms, nach dem der Cache zurückgesetzt werden soll (z.B. 5 Minuten)
async registerWithEmail(email: string, password: string): Promise<UserCredential> { 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 // Bestimmen der aktuellen Umgebung/Domain für die Verifizierungs-URL
let verificationUrl = ''; let verificationUrl = '';
@ -35,7 +71,7 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
// ActionCode-Einstellungen mit der dynamischen URL // ActionCode-Einstellungen mit der dynamischen URL
const actionCodeSettings = { const actionCodeSettings = {
url: `${verificationUrl}?email=${email}`, url: `${verificationUrl}?email=${email}`,
handleCodeInApp: true handleCodeInApp: true,
}; };
// Benutzer erstellen // Benutzer erstellen
@ -49,23 +85,22 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
console.log('Verification email sent successfully'); console.log('Verification email sent successfully');
// Erfolgsmeldung anzeigen // Erfolgsmeldung anzeigen
}, },
error: (error) => { error: error => {
console.error('Error sending verification email', error); console.error('Error sending verification email', error);
// Fehlermeldung anzeigen // Fehlermeldung anzeigen
} },
}); });
} }
// Token, RefreshToken und ggf. photoURL speichern // const token = await userCredential.user.getIdToken();
const token = await userCredential.user.getIdToken(); // localStorage.setItem('authToken', token);
localStorage.setItem('authToken', token); // localStorage.setItem('refreshToken', userCredential.user.refreshToken);
localStorage.setItem('refreshToken', userCredential.user.refreshToken); // if (userCredential.user.photoURL) {
if (userCredential.user.photoURL) { // localStorage.setItem('photoURL', userCredential.user.photoURL);
localStorage.setItem('photoURL', userCredential.user.photoURL); // }
}
return userCredential; return userCredential;
} }
// Login mit Email und Passwort // Login mit Email und Passwort
loginWithEmail(email: string, password: string): Promise<UserCredential> { loginWithEmail(email: string, password: string): Promise<UserCredential> {
@ -77,6 +112,7 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
if (userCredential.user.photoURL) { if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL); localStorage.setItem('photoURL', userCredential.user.photoURL);
} }
this.loadRoleFromToken();
} }
return userCredential; return userCredential;
}); });
@ -93,6 +129,7 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
if (userCredential.user.photoURL) { if (userCredential.user.photoURL) {
localStorage.setItem('photoURL', userCredential.user.photoURL); localStorage.setItem('photoURL', userCredential.user.photoURL);
} }
this.loadRoleFromToken();
} }
return userCredential; return userCredential;
}); });
@ -103,9 +140,74 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
localStorage.removeItem('authToken'); localStorage.removeItem('authToken');
localStorage.removeItem('refreshToken'); localStorage.removeItem('refreshToken');
localStorage.removeItem('photoURL'); localStorage.removeItem('photoURL');
this.clearRoleCache();
this.userRoleSubject.next(null);
return this.auth.signOut(); 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) // Prüft, ob ein Token noch gültig ist (über die "exp"-Eigenschaft)
private isTokenValid(token: string): boolean { private isTokenValid(token: string): boolean {
try { try {

View File

@ -1,10 +1,10 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { PaymentMethod } from '@stripe/stripe-js'; import { PaymentMethod } from '@stripe/stripe-js';
import { catchError, forkJoin, lastValueFrom, map, Observable, of, Subject } from 'rxjs'; import { catchError, forkJoin, lastValueFrom, map, Observable, of, Subject } from 'rxjs';
import urlcat from 'urlcat'; import urlcat from 'urlcat';
import { User } from '../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../bizmatch-server/src/models/db.model';
import { CombinedUser, KeycloakUser, ResponseUsersArray, StripeSubscription, StripeUser, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model'; import { CombinedUser, FirebaseUserInfo, KeycloakUser, ResponseUsersArray, StripeSubscription, StripeUser, UserListingCriteria, UserRole, UsersResponse } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
@ -56,6 +56,41 @@ export class UserService {
// ------------------------------- // -------------------------------
// ADMIN SERVICES // ADMIN SERVICES
// ------------------------------- // -------------------------------
/**
* Ruft alle Benutzer mit Paginierung ab
*/
getAllUsers(maxResults?: number, pageToken?: string): Observable<UsersResponse> {
let params = new HttpParams();
if (maxResults) {
params = params.set('maxResults', maxResults.toString());
}
if (pageToken) {
params = params.set('pageToken', pageToken);
}
return this.http.get<UsersResponse>(`${this.apiBaseUrl}/bizmatch/auth`, { params });
}
/**
* Ruft Benutzer mit einer bestimmten Rolle ab
*/
getUsersByRole(role: UserRole): Observable<{ users: FirebaseUserInfo[] }> {
return this.http.get<{ users: FirebaseUserInfo[] }>(`${this.apiBaseUrl}/bizmatch/auth/role/${role}`);
}
/**
* Ändert die Rolle eines Benutzers
*/
setUserRole(uid: string, role: UserRole): Observable<{ success: boolean }> {
return this.http.post<{ success: boolean }>(`${this.apiBaseUrl}/${uid}/bizmatch/auth/role`, { role });
}
// -------------------------------
// OLDADMIN SERVICES
// -------------------------------
getKeycloakUsers(): Observable<KeycloakUser[]> { getKeycloakUsers(): Observable<KeycloakUser[]> {
return this.http.get<KeycloakUser[]>(`${this.apiBaseUrl}/bizmatch/auth/user/all`).pipe( return this.http.get<KeycloakUser[]>(`${this.apiBaseUrl}/bizmatch/auth/user/all`).pipe(
catchError(error => { catchError(error => {

View File

@ -340,6 +340,6 @@ export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPro
} }
}); });
} }
export function isAdmin(email: string) { // export function isAdmin(email: string) {
return 'andreas.knuth@gmail.com' === email; // return 'andreas.knuth@gmail.com' === email;
} // }