einbau von rollen, neue Admin Ansicht
This commit is contained in:
parent
dded8b8ca9
commit
5a56b3554d
|
|
@ -75,6 +75,7 @@
|
|||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"kysely-codegen": "^0.15.0",
|
||||
"nest-commander": "^3.16.1",
|
||||
"pg-to-ts": "^4.1.1",
|
||||
"prettier": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
|
|
@ -103,4 +104,4 @@
|
|||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@ import { ConfigModule } from '@nestjs/config';
|
|||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ClsMiddleware, ClsModule } from 'nestjs-cls';
|
||||
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 { UserInterceptor } from './interceptors/user.interceptor';
|
||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { FirebaseAdminModule } from './firebase-admin/firebase-admin.module';
|
||||
|
||||
//loadEnvFiles();
|
||||
console.log('Loaded environment variables:');
|
||||
|
|
@ -82,6 +83,7 @@ console.log('Loaded environment variables:');
|
|||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggingInterceptor, // Registriere den LoggingInterceptor global
|
||||
},
|
||||
AuthService,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
|
|
|||
|
|
@ -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 { 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')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
@Inject('FIREBASE_ADMIN')
|
||||
private readonly firebaseAdmin: typeof admin,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
@Post('verify-email')
|
||||
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);
|
||||
}
|
||||
}
|
||||
@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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import { Module } from '@nestjs/common';
|
|||
import { ConfigModule } from '@nestjs/config';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule.forRoot({ envFilePath: '.env' }),FirebaseAdminModule],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,53 +4,39 @@ import * as admin from 'firebase-admin';
|
|||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
@Inject('FIREBASE_ADMIN')
|
||||
private readonly firebaseAdmin: typeof admin,
|
||||
@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('No token provided');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('Missing or invalid authorization token');
|
||||
}
|
||||
|
||||
const token = authHeader.split('Bearer ')[1];
|
||||
|
||||
try {
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
// @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;
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,54 +3,70 @@ import * as admin from 'firebase-admin';
|
|||
|
||||
@Injectable()
|
||||
export class OptionalAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
@Inject('FIREBASE_ADMIN')
|
||||
private readonly firebaseAdmin: typeof admin,
|
||||
) {}
|
||||
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!token) {
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
//throw new UnauthorizedException('Missing or invalid authorization token');
|
||||
return true;
|
||||
}
|
||||
|
||||
const token = authHeader.split('Bearer ')[1];
|
||||
|
||||
try {
|
||||
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;
|
||||
} catch (error) {
|
||||
//throw new UnauthorizedException('Invalid token');
|
||||
request['user'] = null;
|
||||
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()
|
||||
// export class OptionalAuthGuard implements CanActivate {
|
||||
// constructor(
|
||||
// @Inject('FIREBASE_ADMIN')
|
||||
// private readonly firebaseAdmin: typeof admin,
|
||||
// ) {}
|
||||
|
||||
// async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// const request = context.switchToHttp().getRequest<Request>();
|
||||
// const token = this.extractTokenFromHeader(request);
|
||||
|
||||
// if (!token) {
|
||||
// return true; // Kein Token vorhanden, aber Zugriff erlaubt
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const decodedToken = await admin.auth().verifyIdToken(token);
|
||||
// request['user'] = decodedToken; // Benutzerdaten zum Request hinzufügen, wenn Token gültig
|
||||
// const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
|
||||
// request['user'] = decodedToken;
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// // Bei ungültigem Token wird kein Fehler geworfen, sondern einfach kein User gesetzt
|
||||
// //throw new UnauthorizedException('Invalid token');
|
||||
// request['user'] = null;
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// return true; // Zugriff wird immer erlaubt
|
||||
// }
|
||||
|
||||
// private extractTokenFromHeader(request: Request): string | undefined {
|
||||
|
|
|
|||
|
|
@ -278,6 +278,26 @@ export interface Checkout {
|
|||
email: 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 {
|
||||
// Check for undefined or null
|
||||
if (value === undefined || value === null) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { Logger } from 'winston';
|
|||
import { ZodError } from 'zod';
|
||||
import { FileService } from '../file/file.service';
|
||||
|
||||
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { User } from '../models/db.model';
|
||||
|
|
@ -31,11 +32,11 @@ export class UserController {
|
|||
const user = await this.userService.getUserById(id);
|
||||
return user;
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Get('user/all')
|
||||
// async getAllUser(): Promise<User[]> {
|
||||
// return await this.userService.getAllUser();
|
||||
// }
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('user/all')
|
||||
async getAllUser(): Promise<User[]> {
|
||||
return await this.userService.getAllUser();
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
<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>
|
||||
</li>
|
||||
@if(user.customerType==='professional' || user.customerType==='seller' || isAdmin()){
|
||||
@if(user.customerType==='professional' || user.customerType==='seller' || (authService.isAdmin() | async)){
|
||||
<li>
|
||||
@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"
|
||||
|
|
@ -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>
|
||||
</li>
|
||||
</ul>
|
||||
@if(isAdmin()){
|
||||
@if(authService.isAdmin() | async){
|
||||
<ul class="py-2">
|
||||
<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>
|
||||
|
|
@ -121,8 +121,7 @@
|
|||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
@if ((numberOfBroker$ | async) > 0) {
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
|
|
@ -165,8 +164,7 @@
|
|||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
@if ((numberOfBroker$ | async) > 0) {
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
|
|
@ -219,8 +217,7 @@
|
|||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
@if ((numberOfBroker$ | async) > 0) {
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { SearchService } from '../../services/search.service';
|
|||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { SharedService } from '../../services/shared.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 { ModalService } from '../search-modal/modal.service';
|
||||
@UntilDestroy()
|
||||
|
|
@ -58,8 +58,8 @@ export class HeaderComponent {
|
|||
private searchService: SearchService,
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
public selectOptions: SelectOptionsService,
|
||||
private authService: AuthService,
|
||||
private listingService: ListingsService,
|
||||
public authService: AuthService,
|
||||
private listingService: ListingsService,
|
||||
) {}
|
||||
@HostListener('document:click', ['$event'])
|
||||
handleGlobalClick(event: Event) {
|
||||
|
|
@ -76,7 +76,7 @@ export class HeaderComponent {
|
|||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||
}
|
||||
this.numberOfBroker$ = this.userService.getNumberOfBroker(createEmptyUserListingCriteria());
|
||||
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
|
||||
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 10);
|
||||
|
|
@ -198,7 +198,4 @@ export class HeaderComponent {
|
|||
toggleSortDropdown() {
|
||||
this.sortDropdownVisible = !this.sortDropdownVisible;
|
||||
}
|
||||
isAdmin() {
|
||||
return isAdmin(this.user.email);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,156 +1,154 @@
|
|||
<!-- src/app/components/user-list/user-list.component.html -->
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-4">Benutzerverwaltung</h1>
|
||||
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
||||
<h2 class="text-2xl font-bold text-gray-800 mb-6">Benutzerverwaltung</h2>
|
||||
|
||||
<!-- Ladeanzeige -->
|
||||
<div *ngIf="isLoading" class="flex justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||
<!-- Rollenfilter -->
|
||||
<div class="mb-6">
|
||||
<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>
|
||||
|
||||
<!-- Fehlermeldung -->
|
||||
<div *ngIf="error" class="text-red-500 mb-4">
|
||||
{{ error }}
|
||||
<div *ngIf="error" class="bg-red-50 border border-red-200 text-red-800 rounded-md p-4 mb-6 relative">
|
||||
<span class="block sm:inline">{{ error }}</span>
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Benutzer-Tabelle -->
|
||||
<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>
|
||||
</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>
|
||||
<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> 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> Messages</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Ladeanzeige -->
|
||||
<div *ngIf="loading" class="flex justify-center my-8">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Flowbite Modal für Kreditkarteninformationen -->
|
||||
<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 class="relative w-full max-w-2xl max-h-full">
|
||||
<!-- Modal-Content -->
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<!-- Modal-Kopf -->
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Kreditkarteninformationen</h3>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
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"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</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>
|
||||
<!-- Benutzertabelle -->
|
||||
<div class="overflow-x-auto shadow-md rounded-lg bg-white">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<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
|
||||
}"
|
||||
>
|
||||
{{ 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
|
||||
fill-rule="evenodd"
|
||||
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"
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="ccInfoError" class="text-red-500">
|
||||
{{ ccInfoError }}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-template #noCCInfo>
|
||||
<p>Keine Kreditkarteninformationen verfügbar.</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal-Fuß -->
|
||||
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
<!-- Keine Benutzer gefunden -->
|
||||
<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>
|
||||
|
||||
<!-- "Mehr laden"-Button -->
|
||||
<div *ngIf="hasMoreUsers" class="flex justify-center mt-6 mb-8">
|
||||
<button
|
||||
(click)="loadMoreUsers()"
|
||||
[disabled]="loading"
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,138 +1,97 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
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 { FormsModule } from '@angular/forms';
|
||||
import { FirebaseUserInfo, UserRole, UsersResponse } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TooltipComponent],
|
||||
providers: [DatePipe],
|
||||
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 {
|
||||
combinedUsers: CombinedUser[] = [];
|
||||
isLoading = true;
|
||||
users: FirebaseUserInfo[] = [];
|
||||
loading = false;
|
||||
error: string | null = null;
|
||||
selectedUser: CombinedUser | null = null;
|
||||
creditCardInfo: PaymentMethod[] = [];
|
||||
ccInfoLoading = false;
|
||||
ccInfoError: string | null = null;
|
||||
showModal = false;
|
||||
constructor(private userService: UserService, private datePipe: DatePipe, private confirmationService: ConfirmationService, private messageService: MessageService) {}
|
||||
|
||||
// Paginierung
|
||||
pageToken?: string;
|
||||
hasMoreUsers = false;
|
||||
maxResultsPerPage = 50;
|
||||
|
||||
// Filterung
|
||||
selectedRole: UserRole | 'all' = 'all';
|
||||
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadUsers();
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
// initFlowbite();
|
||||
}
|
||||
|
||||
loadUsers(): void {
|
||||
this.userService.loadUsers().subscribe({
|
||||
next: users => {
|
||||
this.combinedUsers = users;
|
||||
this.isLoading = false;
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 10);
|
||||
},
|
||||
error: err => {
|
||||
this.error = 'Fehler beim Laden der Benutzer';
|
||||
this.isLoading = 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;
|
||||
}
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
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;
|
||||
if (this.selectedRole !== 'all') {
|
||||
// Benutzer nach Rolle filtern
|
||||
this.userService.getUsersByRole(this.selectedRole).subscribe({
|
||||
next: response => {
|
||||
this.users = response.users;
|
||||
this.loading = false;
|
||||
this.hasMoreUsers = false; // Bei Rollenfilterung keine Paginierung
|
||||
},
|
||||
error: err => {
|
||||
this.ccInfoError = 'Fehler beim Laden der Kreditkarteninformationen';
|
||||
this.ccInfoLoading = false;
|
||||
console.error(err);
|
||||
this.error = 'Fehler beim Laden der Benutzer: ' + (err.message || err);
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.ccInfoError = 'Keine gültige E-Mail-Adresse gefunden';
|
||||
this.ccInfoLoading = false;
|
||||
// Alle Benutzer mit Paginierung laden
|
||||
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 {}
|
||||
closeModal(): void {
|
||||
this.showModal = false;
|
||||
this.selectedUser = null;
|
||||
this.creditCardInfo = [];
|
||||
this.ccInfoError = null;
|
||||
loadMoreUsers(): void {
|
||||
if (this.hasMoreUsers && !this.loading) {
|
||||
this.loadUsers();
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { MailService } from '../../../services/mail.service';
|
|||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { createMailInfo, isAdmin, map2User } from '../../../utils/utils';
|
||||
import { createMailInfo, map2User } from '../../../utils/utils';
|
||||
// Import für Leaflet
|
||||
// Benannte Importe für Leaflet
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
|
|
@ -79,7 +79,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
|||
private auditService: AuditService,
|
||||
public emailService: EMailService,
|
||||
private geoService: GeoService,
|
||||
private authService: AuthService,
|
||||
public authService: AuthService,
|
||||
) {
|
||||
super();
|
||||
this.router.events.subscribe(event => {
|
||||
|
|
@ -115,9 +115,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
|||
ngOnDestroy() {
|
||||
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
|
||||
}
|
||||
isAdmin() {
|
||||
return isAdmin(this.keycloakUser?.email); //this.keycloakService.getUserRoles(true).includes('ADMIN');
|
||||
}
|
||||
|
||||
async mail() {
|
||||
try {
|
||||
this.mailinfo.email = this.listingUser.email;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { MailService } from '../../../services/mail.service';
|
|||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
|
|
@ -83,7 +83,7 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||
private messageService: MessageService,
|
||||
private auditService: AuditService,
|
||||
private emailService: EMailService,
|
||||
private authService: AuthService,
|
||||
public authService: AuthService,
|
||||
) {
|
||||
super();
|
||||
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
|
||||
|
|
@ -139,9 +139,6 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
|||
.catch(error => console.error('Error initializing Flowbite:', error));
|
||||
});
|
||||
}
|
||||
isAdmin() {
|
||||
return isAdmin(this.keycloakUser?.email);
|
||||
}
|
||||
async mail() {
|
||||
try {
|
||||
this.mailinfo.email = this.listingUser.email;
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@
|
|||
</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>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { ListingsService } from '../../../services/listings.service';
|
|||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { formatPhoneNumber, isAdmin, map2User } from '../../../utils/utils';
|
||||
import { formatPhoneNumber, map2User } from '../../../utils/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-user',
|
||||
|
|
@ -45,7 +45,7 @@ export class DetailsUserComponent {
|
|||
private sanitizer: DomSanitizer,
|
||||
private imageService: ImageService,
|
||||
public historyService: HistoryService,
|
||||
private authService: AuthService,
|
||||
public authService: AuthService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
|
@ -59,8 +59,4 @@ export class DetailsUserComponent {
|
|||
this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview ? this.user.companyOverview : '');
|
||||
this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices ? this.user.offeredServices : '');
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return isAdmin(this.user.email);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export class HomeComponent {
|
|||
initFlowbite();
|
||||
}, 0);
|
||||
this.numberOfBroker$ = this.userService.getNumberOfBroker(createEmptyUserListingCriteria());
|
||||
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
|
||||
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
|
||||
const token = await this.authService.getToken();
|
||||
sessionStorage.removeItem('businessListings');
|
||||
sessionStorage.removeItem('commercialPropertyListings');
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
<p class="text-xs text-gray-500 mt-1">You can only modify your email by contacting us at support@bizmatch.net</p>
|
||||
</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 h-full justify-between flex-col">
|
||||
<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>
|
||||
</select>
|
||||
</div> -->
|
||||
@if (isAdmin() && !id){
|
||||
@if ((authService.isAdmin() | async) && !id){
|
||||
<div>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { SelectOptionsService } from '../../../services/select-options.service';
|
|||
import { SharedService } from '../../../services/shared.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { isAdmin, map2User } from '../../../utils/utils';
|
||||
import { map2User } from '../../../utils/utils';
|
||||
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
|
|
@ -96,7 +96,7 @@ export class AccountComponent {
|
|||
// private subscriptionService: SubscriptionsService,
|
||||
private datePipe: DatePipe,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
public authService: AuthService,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
setTimeout(() => {
|
||||
|
|
@ -264,9 +264,7 @@ export class AccountComponent {
|
|||
const message = this.validationMessages.find(msg => msg.field === fieldName);
|
||||
return message ? message.message : '';
|
||||
}
|
||||
isAdmin() {
|
||||
return isAdmin(this.user.email);
|
||||
}
|
||||
|
||||
setState(index: number, state: string) {
|
||||
if (state === null) {
|
||||
this.user.areasServed[index].county = null;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ 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 { firstValueFrom } from 'rxjs';
|
||||
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',
|
||||
})
|
||||
|
|
@ -15,57 +17,90 @@ export class AuthService {
|
|||
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;
|
||||
|
||||
// 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';
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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
|
||||
private loadRoleFromToken(): void {
|
||||
this.getToken().then(token => {
|
||||
if (token) {
|
||||
const role = this.extractRoleFromToken(token);
|
||||
this.userRoleSubject.next(role);
|
||||
} else {
|
||||
this.userRoleSubject.next(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Token, RefreshToken und ggf. photoURL speichern
|
||||
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);
|
||||
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 = '';
|
||||
|
||||
return userCredential;
|
||||
}
|
||||
// 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> {
|
||||
|
|
@ -77,6 +112,7 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
|
|||
if (userCredential.user.photoURL) {
|
||||
localStorage.setItem('photoURL', userCredential.user.photoURL);
|
||||
}
|
||||
this.loadRoleFromToken();
|
||||
}
|
||||
return userCredential;
|
||||
});
|
||||
|
|
@ -93,6 +129,7 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
|
|||
if (userCredential.user.photoURL) {
|
||||
localStorage.setItem('photoURL', userCredential.user.photoURL);
|
||||
}
|
||||
this.loadRoleFromToken();
|
||||
}
|
||||
return userCredential;
|
||||
});
|
||||
|
|
@ -103,9 +140,74 @@ async registerWithEmail(email: string, password: string): Promise<UserCredential
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -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 { PaymentMethod } from '@stripe/stripe-js';
|
||||
import { catchError, forkJoin, lastValueFrom, map, Observable, of, Subject } from 'rxjs';
|
||||
import urlcat from 'urlcat';
|
||||
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';
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -56,6 +56,41 @@ export class UserService {
|
|||
// -------------------------------
|
||||
// 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[]> {
|
||||
return this.http.get<KeycloakUser[]>(`${this.apiBaseUrl}/bizmatch/auth/user/all`).pipe(
|
||||
catchError(error => {
|
||||
|
|
|
|||
|
|
@ -340,6 +340,6 @@ export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPro
|
|||
}
|
||||
});
|
||||
}
|
||||
export function isAdmin(email: string) {
|
||||
return 'andreas.knuth@gmail.com' === email;
|
||||
}
|
||||
// export function isAdmin(email: string) {
|
||||
// return 'andreas.knuth@gmail.com' === email;
|
||||
// }
|
||||
|
|
|
|||
Loading…
Reference in New Issue