Compare commits
10 Commits
b9a9b983e9
...
noKeycloak
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a56b3554d | |||
| dded8b8ca9 | |||
| b55447cd3f | |||
| e37613ffa0 | |||
| d8c48bf58a | |||
| 4c19356188 | |||
| 27242819e2 | |||
| 521e799bff | |||
| f6d1b8623c | |||
| a2e6243e93 |
1
bizmatch-server/.gitignore
vendored
1
bizmatch-server/.gitignore
vendored
@@ -58,6 +58,7 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
pictures
|
||||
pictures_base
|
||||
pictures_
|
||||
|
||||
src/*.js
|
||||
bun.lockb
|
||||
|
||||
16
bizmatch-server/.vscode/launch.json
vendored
16
bizmatch-server/.vscode/launch.json
vendored
@@ -5,7 +5,8 @@
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Nest Framework",
|
||||
"runtimeExecutable": "npm",
|
||||
//"runtimeExecutable": "npm",
|
||||
"runtimeExecutable": "/home/aknuth/.nvm/versions/node/v22.14.0/bin/npm",
|
||||
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"restart": true,
|
||||
@@ -13,17 +14,20 @@
|
||||
"stopOnEntry": false,
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"HOST_NAME": "localhost"
|
||||
},
|
||||
"preLaunchTask": "Start Stripe Listener"
|
||||
"HOST_NAME": "localhost",
|
||||
"FIREBASE_PROJECT_ID": "bizmatch-net",
|
||||
"FIREBASE_CLIENT_EMAIL": "firebase-adminsdk-fbsvc@bizmatch-net.iam.gserviceaccount.com",
|
||||
"FIREBASE_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsOlDmhG0zi1zh\nlvobM8yAmLDR3P0F7mHcLyAga2rZm9MnPiGcmkoqRtDnxpZXio36PiyEgdKyhJFK\nP+jPJx1Zo/Ko9vb983oCGcz6MWgRKFXwLT4UJXjwjBdNDe/gcl52c+JJtZJR4bwD\n/bBgkoLzU9lF97pJoQypkSXytyxea6yrS2oEDs7SjW7z9JGFsoxFrt7zbMRb8tIs\nyCWe4I9YSgjSrwOw2uXpdrV0qjDkjx1TokuVJHDH9Vi8XhXDBx9y87Ja0hBoYDE9\nJJRLAa70qHQ9ytfdH/H0kucptC1JkdYGmLQHbohoPDuTU/C85JZvqIitwJ4YEH6Y\nfd+gEe5TAgMBAAECggEALrKDI/WNDFhBn1MJzl1dmhKMguKJ4lVPyF0ot1GYv5bu\nCipg/66f5FWeJ/Hi6qqBM3QvKuBuagPixwCMFbrTzO3UijaoIpQlJTOsrbu+rURE\nBOKnfdvpLkO1v6lDPJaWAUULepPWMAhmK6jZ7V1cTzCRbVSteHBH2CQoZ2Z+C71w\nyvzAIr6JRSg4mYbtHrQCXx9odPCRTdiRvxu5QtihiZGFSXnkTfhDNL1DKff7XHKF\nbOaDPumGtE7ypXr+0qyefg8xeTmXxdI4lPdqxd8XTpLFdMU8nW+/sEjdR40G8ikf\nt6nwyMh01YMMNi88t7ZoDvhpLALb4OqHBhDmyMdOWQKBgQDm5I0cqYX18jypC32G\nUhOdOou6IaZlVDNztZUhFPHPrP0P5Qg1PE5E5YybV7GVNXWiNwI/MPPF0JBce/Ie\ngJoXnuQ9kLh7cNZ432Jhz/Nmhytr6RGxoykAMT1fCuVLsTCfuK4e/aDAgVFJ84gS\nsB3TA62t2hak2MMntKoAQeDwWwKBgQC+9K+MRI/Vj1Xl7jwJ+adRQIvOssVz74ZE\nRYwIDZNRdk/c7c63WVHXASCRZbroGvqJgVfnmtwR6XJTnW3tkYqKUl5W9E+FSVbf\ng4aZs1oaVMA/IirVlRbJ4oCT+nDxPPuJ3ceJ4mBcODO82zXaC6pSFCvkpz9k9lc3\nUPlTLk1baQKBgFMbLqODbSFSeH0MErlXL5InMYXkeMT+IqriT/QhWsw6Yrfm4yZu\nN2nbCdocHWIsZNPnYtql3whzgpKXVlWeSlh4K4TxY0WjHr9RAFNeiyh7PKjRsjmz\nFZ3pG0LrZA7zjyHeUmX7OnIv2bd5fZ/kXkfGiiwKVJ4vG0deYtZG4BUDAoGBAJbI\nFRn4RW8HiHdPv37M8E5bXknvpbRfDTE5jVIKjioD9xnneZQTZmkUjcfhgU2nh+8t\n/+B0ypMmN81IgTXW94MzeSTGM0h22a8SZyVUlrA1/bucWiBeYik1vfubBLWoRqLd\nSaNZ6mbHRis5GPO8xFedb+9UFN2/Gq0mNkl1RUYJAoGBALqTxfdr4MXnG6Nhy22V\nWqui9nsHE5RMIvGYBnnq9Kqt8tUEkxB52YkBilx43q/TY4DRMDOeJk2krEbSN3AO\nguTE6BmZacamrt1HIdSAmJ1RktlVDRgIHXMBkBIumCsTCuXaZ+aEjuLOXJDIsIHZ\nEA9ftLrt1h1u+7QPI+E11Fmx\n-----END PRIVATE KEY-----"
|
||||
}
|
||||
// "preLaunchTask": "Start Stripe Listener"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch TypeScript file with tsx",
|
||||
"name": "Launch import from exported with tsx",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["tsx", "--inspect"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/importFromExported.ts"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"],
|
||||
"sourceMaps": true,
|
||||
|
||||
@@ -26,37 +26,26 @@
|
||||
"generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.10.3",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/config": "^4.0.0",
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/platform-express": "^11.0.11",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"body-parser": "^1.20.2",
|
||||
"cls-hooked": "^4.2.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-flow": "^4.1.0",
|
||||
"drizzle-orm": "^0.32.0",
|
||||
"firebase": "^11.3.1",
|
||||
"firebase-admin": "^13.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwk-to-pem": "^2.0.6",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"ky": "^1.4.0",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nestjs-cls": "^4.4.1",
|
||||
"nestjs-cls": "^5.4.0",
|
||||
"nodemailer": "^6.9.10",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"openai": "^4.52.6",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^8.11.5",
|
||||
"pgvector": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
@@ -71,31 +60,22 @@
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/traverse": "^7.24.1",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@nestjs/cli": "^11.0.5",
|
||||
"@nestjs/schematics": "^11.0.1",
|
||||
"@nestjs/testing": "^11.0.11",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/jwk-to-pem": "^2.0.3",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"drizzle-kit": "^0.23.0",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.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",
|
||||
|
||||
@@ -101,23 +101,10 @@ export class AiService {
|
||||
const prompt = `The Search Query of the User is: "${query}"`;
|
||||
let response = null;
|
||||
try {
|
||||
// response = await this.openai.chat.completions.create({
|
||||
// model: 'gpt-4o-mini',
|
||||
// //model: 'gpt-3.5-turbo',
|
||||
// max_tokens: 300,
|
||||
// messages: [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: `Please create unformatted JSON Object from a user input.
|
||||
// The type is: ${JSON.stringify(businessListingCriteriaStructure)}.,
|
||||
// If location details available please fill city, county and state as State Code`,
|
||||
// },
|
||||
// ],
|
||||
// temperature: 0.5,
|
||||
// response_format: { type: 'json_object' },
|
||||
// });
|
||||
|
||||
response = await this.groq.chat.completions.create({
|
||||
response = await this.openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
//model: 'gpt-3.5-turbo',
|
||||
max_tokens: 300,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
@@ -132,15 +119,31 @@ export class AiService {
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
model: 'llama-3.1-70b-versatile',
|
||||
//model: 'llama-3.1-8b-instant',
|
||||
// model: 'mixtral-8x7b-32768',
|
||||
//model: 'gemma2-9b-it',
|
||||
temperature: 0.2,
|
||||
max_tokens: 300,
|
||||
temperature: 0.5,
|
||||
response_format: { type: 'json_object' },
|
||||
});
|
||||
|
||||
// response = await this.groq.chat.completions.create({
|
||||
// messages: [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: `Please create unformatted JSON Object from a user input.
|
||||
// The criteriaType must be only either 'businessListings' or 'commercialPropertyListings' or 'brokerListings' !!!!
|
||||
// The format of the object (depending on your choice of criteriaType) must be either ${BusinessListingCriteriaStructure}, ${CommercialPropertyListingCriteriaStructure} or ${UserListingCriteriaStructure} !!!!
|
||||
// If location details available please fill city and state as State Code and only county if explicitly mentioned.
|
||||
// If you decide for searchType==='exact', please do not set the attribute radius`,
|
||||
// },
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: prompt,
|
||||
// },
|
||||
// ],
|
||||
// model: 'llama-3.3-70b-versatile',
|
||||
// temperature: 0.2,
|
||||
// max_tokens: 300,
|
||||
// response_format: { type: 'json_object' },
|
||||
// });
|
||||
|
||||
const generatedCriteria = JSON.parse(response.choices[0]?.message?.content);
|
||||
return generatedCriteria;
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard';
|
||||
|
||||
import { AuthGuard } from './jwt-auth/auth.guard';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(
|
||||
private readonly appService: AppService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Get()
|
||||
getHello(@Request() req): string {
|
||||
return req.user;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import { AiModule } from './ai/ai.module';
|
||||
@@ -14,34 +12,34 @@ import { ListingsModule } from './listings/listings.module';
|
||||
import { LogController } from './log/log.controller';
|
||||
import { LogModule } from './log/log.module';
|
||||
|
||||
import dotenvFlow from 'dotenv-flow';
|
||||
import { EventModule } from './event/event.module';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { MailModule } from './mail/mail.module';
|
||||
|
||||
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 { PaymentModule } from './payment/payment.module';
|
||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
|
||||
//loadEnvFiles();
|
||||
dotenvFlow.config();
|
||||
console.log('Loaded environment variables:');
|
||||
console.log(JSON.stringify(process.env, null, 2));
|
||||
//console.log(JSON.stringify(process.env, null, 2));
|
||||
@Module({
|
||||
imports: [
|
||||
ClsModule.forRoot({
|
||||
global: true, // Macht den ClsService global verfügbar
|
||||
middleware: { mount: true }, // Registriert automatisch die ClsMiddleware
|
||||
// setup: clsService => {
|
||||
// // Optional: zusätzliche Setup-Logik
|
||||
// },
|
||||
}),
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
//ConfigModule.forRoot({ envFilePath: '.env' }),
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: [path.resolve(__dirname, '..', '.env')],
|
||||
}),
|
||||
MailModule,
|
||||
AuthModule,
|
||||
WinstonModule.forRoot({
|
||||
@@ -67,17 +65,16 @@ console.log(JSON.stringify(process.env, null, 2));
|
||||
ListingsModule,
|
||||
SelectOptionsModule,
|
||||
ImageModule,
|
||||
PassportModule,
|
||||
AiModule,
|
||||
LogModule,
|
||||
PaymentModule,
|
||||
// PaymentModule,
|
||||
EventModule,
|
||||
FirebaseAdminModule,
|
||||
],
|
||||
controllers: [AppController, LogController],
|
||||
providers: [
|
||||
AppService,
|
||||
FileService,
|
||||
JwtStrategy,
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: UserInterceptor, // Registriere den Interceptor global
|
||||
@@ -86,6 +83,7 @@ console.log(JSON.stringify(process.env, null, 2));
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggingInterceptor, // Registriere den LoggingInterceptor global
|
||||
},
|
||||
AuthService,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
||||
@@ -1,38 +1,129 @@
|
||||
import { Body, Controller, Get, Param, Put, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from 'src/jwt-auth/jwt-auth.guard';
|
||||
import { KeycloakUser } from 'src/models/main.model';
|
||||
import { AdminAuthGuard } from '../jwt-auth/admin-auth.guard';
|
||||
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(private readonly authService: AuthService) {}
|
||||
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) {
|
||||
if (!oobCode || !email) {
|
||||
throw new HttpException('oobCode and email are required', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
try {
|
||||
// Schritt 1: Hole den Benutzer anhand der E-Mail-Adresse
|
||||
const userRecord = await this.firebaseAdmin.auth().getUserByEmail(email);
|
||||
|
||||
if (userRecord.emailVerified) {
|
||||
return { message: 'Email is already verified' };
|
||||
}
|
||||
|
||||
// Schritt 2: Aktualisiere den Benutzerstatus
|
||||
// Hinweis: Wir können den oobCode nicht serverseitig validieren.
|
||||
// Wir nehmen an, dass der oobCode korrekt ist, da er von Firebase generiert wurde.
|
||||
await this.firebaseAdmin.auth().updateUser(userRecord.uid, {
|
||||
emailVerified: true,
|
||||
});
|
||||
|
||||
return { message: 'Email successfully verified' };
|
||||
} catch (error) {
|
||||
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()
|
||||
async getAccessToken(): Promise<any> {
|
||||
return await this.authService.getAccessToken();
|
||||
@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 }),
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('user/all')
|
||||
async getUsers(): Promise<any> {
|
||||
return await this.authService.getUsers();
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('users/:userid')
|
||||
async getUser(@Param('userid') userId: string): Promise<any> {
|
||||
return await this.authService.getUser(userId);
|
||||
// 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}`,
|
||||
};
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put('users/:userid')
|
||||
async updateKeycloakUser(@Body() keycloakUser: KeycloakUser): Promise<any> {
|
||||
return await this.authService.updateKeycloakUser(keycloakUser);
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
// getLastLogin(@Param('userid') userId: string): any {
|
||||
// return this.authService.getLastLogin(userId);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
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: [PassportModule],
|
||||
providers: [AuthService],
|
||||
imports: [ConfigModule.forRoot({ envFilePath: '.env' }),FirebaseAdminModule],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
providers: [AuthService],
|
||||
exports: [],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,101 +1,113 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { KeycloakUser } from 'src/models/main.model';
|
||||
import urlcat from 'urlcat';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as admin from 'firebase-admin';
|
||||
import { FirebaseUserInfo, UserRole } from 'src/models/main.model';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
public async getAccessToken() {
|
||||
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 {
|
||||
const params = new URLSearchParams();
|
||||
params.append('grant_type', 'password');
|
||||
params.append('username', process.env.KEYCLOAK_ADMIN_USER);
|
||||
params.append('password', process.env.KEYCLOAK_ADMIN_PASSWORD);
|
||||
const URL = `${process.env.KEYCLOAK_HOST}${process.env.KEYCLOAK_TOKEN_URL}`;
|
||||
// Get the current custom claims
|
||||
const user = await this.firebaseAdmin.auth().getUser(uid);
|
||||
const currentClaims = user.customClaims || {};
|
||||
|
||||
const response = await fetch(URL, {
|
||||
method: 'POST',
|
||||
body: params.toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: process.env.KEYCLOAK_ADMIN_TOKEN,
|
||||
},
|
||||
// Set the new role
|
||||
await this.firebaseAdmin.auth().setCustomUserClaims(uid, {
|
||||
...currentClaims,
|
||||
role: role,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return (<any>data).access_token;
|
||||
} catch (error) {
|
||||
if (error.name === 'HTTPError') {
|
||||
const errorJson = await error.response.json();
|
||||
console.error('Fehlerantwort vom Server:', errorJson);
|
||||
} else {
|
||||
console.error('Allgemeiner Fehler:', error);
|
||||
}
|
||||
console.error('Error setting user role:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getUsers(): Promise<KeycloakUser[]> {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.KEYCLOAK_HOST}${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USERS_URL}`;
|
||||
const response = await fetch(URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data as KeycloakUser[];
|
||||
}
|
||||
public async getUser(userid: string): Promise<KeycloakUser> {
|
||||
const token = await this.getAccessToken();
|
||||
const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USER_URL}`;
|
||||
const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid });
|
||||
const response = await fetch(URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data as KeycloakUser;
|
||||
}
|
||||
public async updateKeycloakUser(keycloakUser: KeycloakUser): Promise<void> {
|
||||
const token = await this.getAccessToken();
|
||||
const userid = keycloakUser.id;
|
||||
const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USER_URL}`;
|
||||
const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid });
|
||||
const response = await fetch(URL, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(keycloakUser),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
// public async getLastLogin(userid: string) {
|
||||
// const token = await this.getAccessToken();
|
||||
// const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_LASTLOGIN_URL}`;
|
||||
// const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid });
|
||||
// const response = await ky
|
||||
// .get(URL, {
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
// Authorization: `Bearer ${token}`,
|
||||
// },
|
||||
// })
|
||||
// .json();
|
||||
// return response;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
@@ -9,12 +9,14 @@ import * as schema from './schema';
|
||||
import { PG_CONNECTION } from './schema';
|
||||
const { Pool } = pkg;
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PG_CONNECTION,
|
||||
inject: [ConfigService, WINSTON_MODULE_PROVIDER, ClsService],
|
||||
useFactory: async (configService: ConfigService, logger: Logger, cls: ClsService) => {
|
||||
const connectionString = configService.get<string>('DATABASE_URL');
|
||||
console.log('--->',connectionString)
|
||||
const pool = new Pool({
|
||||
connectionString,
|
||||
// ssl: true, // Falls benötigt
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { promises as fs } from 'fs';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
68
bizmatch-server/src/drizzle/importFromExported.ts
Normal file
68
bizmatch-server/src/drizzle/importFromExported.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { readFileSync } from 'fs';
|
||||
import { Pool } from 'pg';
|
||||
import { BusinessListingService } from 'src/listings/business-listing.service';
|
||||
import { CommercialPropertyService } from 'src/listings/commercial-property.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import winston from 'winston';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from '../models/db.model';
|
||||
import * as schema from './schema';
|
||||
|
||||
(async () => {
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const client = new Pool({ connectionString });
|
||||
const db = drizzle(client, { schema, logger: true });
|
||||
const logger = winston.createLogger({
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
const commService = new CommercialPropertyService(null, db);
|
||||
const businessService = new BusinessListingService(null, db);
|
||||
const userService = new UserService(null, db, null, null);
|
||||
|
||||
//Delete Content
|
||||
await db.delete(schema.commercials);
|
||||
await db.delete(schema.businesses);
|
||||
await db.delete(schema.users);
|
||||
|
||||
let filePath = `./data/users_export.json`;
|
||||
let data: string = readFileSync(filePath, 'utf8');
|
||||
const usersData: User[] = JSON.parse(data); // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < usersData.length; index++) {
|
||||
const user = usersData[index];
|
||||
delete user.id;
|
||||
const u = await userService.saveUser(user, false);
|
||||
logger.info(`user_${index} inserted`);
|
||||
}
|
||||
|
||||
//Corporate Listings
|
||||
filePath = `./data/commercials_export.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < commercialJsonData.length; index++) {
|
||||
const commercial = commercialJsonData[index];
|
||||
delete commercial.id;
|
||||
const result = await commService.createListing(commercial);
|
||||
}
|
||||
|
||||
//Business Listings
|
||||
filePath = `./data/businesses_export.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < businessJsonData.length; index++) {
|
||||
const business = businessJsonData[index];
|
||||
delete business.id;
|
||||
await businessService.createListing(business);
|
||||
}
|
||||
|
||||
//End
|
||||
await client.end();
|
||||
})();
|
||||
function getRandomItem<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('The array is empty.');
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
return arr[randomIndex];
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pkg from 'pg';
|
||||
import * as schema from './schema';
|
||||
const { Pool } = pkg;
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const pool = new Pool({ connectionString });
|
||||
const db = drizzle(pool, { schema });
|
||||
// This will run migrations on the database, skipping the ones already applied
|
||||
//await migrate(db, { migrationsFolder: './src/drizzle/migrations' });
|
||||
// Don't forget to close the connection, otherwise the script will hang
|
||||
//await pool.end();
|
||||
@@ -33,6 +33,7 @@ export const users = pgTable(
|
||||
subscriptionId: text('subscriptionId'),
|
||||
subscriptionPlan: subscriptionTypeEnum('subscriptionPlan'),
|
||||
location: jsonb('location'),
|
||||
showInDirectory: boolean('showInDirectory').default(true),
|
||||
// city: varchar('city', { length: 255 }),
|
||||
// state: char('state', { length: 2 }),
|
||||
// latitude: doublePrecision('latitude'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common';
|
||||
import { RealIp } from 'src/decorators/real-ip.decorator';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { ListingEvent } from 'src/models/db.model';
|
||||
import { RealIpInfo } from 'src/models/main.model';
|
||||
import { EventService } from './event.service';
|
||||
@@ -9,7 +9,7 @@ import { EventService } from './event.service';
|
||||
export class EventController {
|
||||
constructor(private eventService: EventService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
async createEvent(
|
||||
@Body() event: ListingEvent, // Struktur des Body-Objekts entsprechend anpassen
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DrizzleModule } from 'src/drizzle/drizzle.module';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { EventController } from './event.controller';
|
||||
import { EventService } from './event.service';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule],
|
||||
imports: [DrizzleModule,FirebaseAdminModule],
|
||||
controllers: [EventController],
|
||||
providers: [EventService],
|
||||
})
|
||||
|
||||
30
bizmatch-server/src/firebase-admin/firebase-admin.module.ts
Normal file
30
bizmatch-server/src/firebase-admin/firebase-admin.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: 'FIREBASE_ADMIN',
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const serviceAccount = {
|
||||
projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
|
||||
clientEmail: configService.get<string>('FIREBASE_CLIENT_EMAIL'),
|
||||
privateKey: configService.get<string>('FIREBASE_PRIVATE_KEY')?.replace(/\\n/g, '\n'),
|
||||
};
|
||||
|
||||
if (!admin.apps.length) {
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
});
|
||||
}
|
||||
|
||||
return admin;
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: ['FIREBASE_ADMIN'],
|
||||
})
|
||||
export class FirebaseAdminModule {}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { RealIp } from 'src/decorators/real-ip.decorator';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { RealIpInfo } from 'src/models/main.model';
|
||||
import { CountyRequest } from 'src/models/server.model';
|
||||
import { GeoService } from './geo.service';
|
||||
@@ -9,31 +9,31 @@ import { GeoService } from './geo.service';
|
||||
export class GeoController {
|
||||
constructor(private geoService: GeoService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':prefix')
|
||||
findByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('citiesandstates/:prefix')
|
||||
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesAndStatesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':prefix/:state')
|
||||
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix, state);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('counties')
|
||||
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
|
||||
return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('ipinfo/georesult/wysiwyg')
|
||||
async fetchIpAndGeoLocation(@RealIp() ipInfo: RealIpInfo): Promise<any> {
|
||||
return await this.geoService.fetchIpAndGeoLocation(ipInfo);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { GeoController } from './geo.controller';
|
||||
import { GeoService } from './geo.service';
|
||||
|
||||
@Module({
|
||||
imports: [FirebaseAdminModule],
|
||||
controllers: [GeoController],
|
||||
providers: [GeoService],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { JwtAuthGuard } from 'src/jwt-auth/jwt-auth.guard';
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { CommercialPropertyService } from '../listings/commercial-property.service';
|
||||
@@ -18,14 +18,14 @@ export class ImageController {
|
||||
// ############
|
||||
// Property
|
||||
// ############
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('uploadPropertyPicture/:imagePath/:serial')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
|
||||
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
|
||||
await this.listingService.addImage(imagePath, serial, imagename);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
|
||||
@@ -34,13 +34,13 @@ export class ImageController {
|
||||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('uploadProfile/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('profile/:email/')
|
||||
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||
@@ -48,13 +48,13 @@ export class ImageController {
|
||||
// ############
|
||||
// Logo
|
||||
// ############
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('uploadCompanyLogo/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('logo/:email/')
|
||||
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ListingsModule } from '../listings/listings.module';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||
@@ -6,7 +7,7 @@ import { ImageController } from './image.controller';
|
||||
import { ImageService } from './image.service';
|
||||
|
||||
@Module({
|
||||
imports: [ListingsModule],
|
||||
imports: [ListingsModule,FirebaseAdminModule],
|
||||
controllers: [ImageController],
|
||||
providers: [ImageService, FileService, SelectOptionsService],
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ export class LoggingInterceptor implements NestInterceptor {
|
||||
|
||||
const ip = this.cls.get('ip') || 'unknown';
|
||||
const countryCode = this.cls.get('countryCode') || 'unknown';
|
||||
const username = this.cls.get('username') || 'unknown';
|
||||
const username = this.cls.get('email') || 'unknown';
|
||||
|
||||
const method = request.method;
|
||||
const url = request.originalUrl;
|
||||
|
||||
@@ -13,12 +13,12 @@ export class UserInterceptor implements NestInterceptor {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
// Überprüfe, ob der Benutzer authentifiziert ist
|
||||
if (request.user && request.user.username) {
|
||||
if (request.user && request.user.email) {
|
||||
try {
|
||||
this.cls.set('username', request.user.username);
|
||||
this.logger.log(`CLS context gesetzt: Username=${request.user.username}`);
|
||||
this.cls.set('email', request.user.email);
|
||||
this.logger.log(`CLS context gesetzt: EMail=${request.user.email}`);
|
||||
} catch (error) {
|
||||
this.logger.error('Fehler beim Setzen der Username im CLS-Kontext', error);
|
||||
this.logger.error('Fehler beim Setzen der EMail im CLS-Kontext', error);
|
||||
}
|
||||
} else {
|
||||
this.logger.log('Kein authentifizierter Benutzer gefunden');
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthGuard extends AuthGuard('jwt') implements CanActivate {
|
||||
canActivate(context: ExecutionContext) {
|
||||
// Add your custom authentication logic here
|
||||
// for example, call super.logIn(request) to establish a session.
|
||||
return super.canActivate(context);
|
||||
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');
|
||||
}
|
||||
handleRequest(err, user, info) {
|
||||
// You can throw an exception based on either "info" or "err" arguments
|
||||
if (err || !user || !user.roles.includes('ADMIN')) {
|
||||
throw err || new UnauthorizedException(info);
|
||||
|
||||
if (request.user.role !== 'admin') {
|
||||
throw new ForbiddenException('Requires admin privileges');
|
||||
}
|
||||
return user;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
42
bizmatch-server/src/jwt-auth/auth.guard.ts
Normal file
42
bizmatch-server/src/jwt-auth/auth.guard.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
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);
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
|
||||
canActivate(context: ExecutionContext) {
|
||||
// Add your custom authentication logic here
|
||||
// for example, call super.logIn(request) to establish a session.
|
||||
return super.canActivate(context);
|
||||
}
|
||||
handleRequest(err, user, info) {
|
||||
// You can throw an exception based on either "info" or "err" arguments
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException(info);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
21
bizmatch-server/src/jwt-auth/localhost.guard.ts
Normal file
21
bizmatch-server/src/jwt-auth/localhost.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
76
bizmatch-server/src/jwt-auth/optional-auth.guard.ts
Normal file
76
bizmatch-server/src/jwt-auth/optional-auth.guard.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
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 firebaseAdmin: admin.app.App) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
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);
|
||||
|
||||
// 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');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
|
||||
// request['user'] = decodedToken;
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
|
||||
handleRequest(err, user, info) {
|
||||
// Wenn der Benutzer nicht authentifiziert ist, aber kein Fehler vorliegt, geben Sie null zurück
|
||||
if (err || !user) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import fs from 'fs';
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
import { JwtPayload, JwtUser } from './models/main.model';
|
||||
// const logger = winston.createLogger({
|
||||
// transports: [new winston.transports.Console()],
|
||||
// });
|
||||
// const pemCache = new Map();
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {
|
||||
const realm = configService.get<string>('REALM');
|
||||
// const staticCerts = loadStaticCerts();
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKeyProvider: passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: false,
|
||||
// jwksRequestsPerMinute: 5,
|
||||
jwksUri: `https://auth.bizmatch.net/realms/${realm}/protocol/openid-connect/certs`,
|
||||
}),
|
||||
audience: 'account', // Keycloak Client ID
|
||||
authorize: '',
|
||||
issuer: `https://auth.bizmatch.net/realms/${realm}`,
|
||||
algorithms: ['RS256'],
|
||||
});
|
||||
}
|
||||
async validate(payload: JwtPayload): Promise<JwtUser> {
|
||||
if (!payload) {
|
||||
this.logger.error('Invalid payload');
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
if (!payload.sub || !payload.preferred_username) {
|
||||
this.logger.error('Missing required claims');
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
const result = { userId: payload.sub, firstname: payload.given_name, lastname: payload.family_name, username: payload.preferred_username, roles: payload.realm_access?.roles };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
export function loadStaticCerts() {
|
||||
const certsPath = path.join(__dirname, '../', 'assets', 'keycloak-certs.json');
|
||||
const certsData = fs.readFileSync(certsPath, 'utf8');
|
||||
return JSON.parse(certsData);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { UserListingCriteria } from 'src/models/main.model';
|
||||
import { Logger } from 'winston';
|
||||
import { UserService } from '../user/user.service';
|
||||
@@ -12,7 +12,7 @@ export class BrokerListingsController {
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('search')
|
||||
async find(@Body() criteria: UserListingCriteria): Promise<any> {
|
||||
return await this.userService.searchUserListings(criteria);
|
||||
|
||||
@@ -103,7 +103,7 @@ export class BusinessListingService {
|
||||
whereConditions.push(and(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
|
||||
}
|
||||
}
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
if (!user?.roles?.includes('ADMIN')) {
|
||||
whereConditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
|
||||
@@ -186,7 +186,7 @@ export class BusinessListingService {
|
||||
|
||||
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
|
||||
const conditions = [];
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
if (!user?.roles?.includes('ADMIN')) {
|
||||
conditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true)));
|
||||
}
|
||||
conditions.push(sql`${businesses.id} = ${id}`);
|
||||
@@ -204,7 +204,7 @@ export class BusinessListingService {
|
||||
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(businesses.email, email));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN'))) {
|
||||
conditions.push(ne(businesses.draft, true));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { JwtAuthGuard } from '../jwt-auth/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { BusinessListing } from '../models/db.model';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
@@ -14,52 +15,52 @@ export class BusinessListingsController {
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
return await this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('favorites/all')
|
||||
async findFavorites(@Request() req): Promise<any> {
|
||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('user/:userid')
|
||||
async findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
return await this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('find')
|
||||
async find(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('findTotal')
|
||||
async findTotal(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<number> {
|
||||
return await this.listingsService.getBusinessListingsCount(criteria, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
async create(@Body() listing: any) {
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Put()
|
||||
async update(@Body() listing: any) {
|
||||
return await this.listingsService.updateBusinessListing(listing.id, listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Delete('listing/:id')
|
||||
async deleteById(@Param('id') id: string) {
|
||||
await this.listingsService.deleteListing(id);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('favorite/:id')
|
||||
async deleteFavorite(@Request() req, @Param('id') id: string) {
|
||||
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGu
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { JwtAuthGuard } from '../jwt-auth/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { CommercialPropertyListing } from '../models/db.model';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
@@ -16,54 +17,54 @@ export class CommercialPropertyListingsController {
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
return await this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('favorites/all')
|
||||
async findFavorites(@Request() req): Promise<any> {
|
||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('user/:email')
|
||||
async findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||
return await this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('find')
|
||||
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('findTotal')
|
||||
async findTotal(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
return await this.listingsService.getCommercialPropertiesCount(criteria, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
async create(@Body() listing: any) {
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Put()
|
||||
async update(@Body() listing: any) {
|
||||
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Delete('listing/:id/:imagePath')
|
||||
async deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
await this.listingsService.deleteListing(id);
|
||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('favorite/:id')
|
||||
async deleteFavorite(@Request() req, @Param('id') id: string) {
|
||||
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
|
||||
|
||||
@@ -49,7 +49,7 @@ export class CommercialPropertyService {
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
|
||||
}
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
if (!user?.roles?.includes('ADMIN')) {
|
||||
whereConditions.push(or(eq(commercials.email, user?.username), ne(commercials.draft, true)));
|
||||
}
|
||||
// whereConditions.push(and(eq(schema.users.customerType, 'professional')));
|
||||
@@ -113,7 +113,7 @@ export class CommercialPropertyService {
|
||||
// #### Find by ID ########################################
|
||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
const conditions = [];
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
if (!user?.roles?.includes('ADMIN')) {
|
||||
conditions.push(or(eq(commercials.email, user?.username), ne(commercials.draft, true)));
|
||||
}
|
||||
conditions.push(sql`${commercials.id} = ${id}`);
|
||||
@@ -132,7 +132,7 @@ export class CommercialPropertyService {
|
||||
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(commercials.email, email));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN'))) {
|
||||
conditions.push(ne(commercials.draft, true));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BrokerListingsController } from './broker-listings.controller';
|
||||
import { BusinessListingsController } from './business-listings.controller';
|
||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller';
|
||||
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
@@ -14,7 +15,7 @@ import { CommercialPropertyService } from './commercial-property.service';
|
||||
import { UnknownListingsController } from './unknown-listings.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, AuthModule, GeoModule],
|
||||
imports: [DrizzleModule, AuthModule, GeoModule,FirebaseAdminModule],
|
||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||
exports: [BusinessListingService, CommercialPropertyService],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Controller, Get, Inject, Param, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
|
||||
@@ -13,7 +13,7 @@ export class UnknownListingsController {
|
||||
private readonly propertyListingsService: CommercialPropertyService,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
try {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Body, Controller, Inject, Post, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
import { LogMessage } from '../models/main.model';
|
||||
@Controller('log')
|
||||
export class LogController {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
log(@Request() req, @Body() message: LogMessage) {
|
||||
if (message.severity === 'info') {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { LogController } from './log.controller';
|
||||
|
||||
@Module({
|
||||
imports: [FirebaseAdminModule],
|
||||
controllers: [LogController],
|
||||
})
|
||||
export class LogModule {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { ShareByEMail, User } from 'src/models/db.model';
|
||||
import { ErrorResponse, MailInfo } from '../models/main.model';
|
||||
import { MailService } from './mail.service';
|
||||
@@ -8,7 +9,7 @@ import { MailService } from './mail.service';
|
||||
export class MailController {
|
||||
constructor(private mailService: MailService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
async sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
if (mailInfo.listing) {
|
||||
@@ -17,14 +18,24 @@ export class MailController {
|
||||
return await this.mailService.sendRequest(mailInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('verify-email')
|
||||
async sendVerificationEmail(@Body() data: {
|
||||
email: string,
|
||||
redirectConfig: {
|
||||
protocol: string,
|
||||
hostname: string,
|
||||
port?: number
|
||||
}
|
||||
}): Promise<void | ErrorResponse> {
|
||||
return await this.mailService.sendVerificationEmail(data.email, data.redirectConfig);
|
||||
}
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('subscriptionConfirmation')
|
||||
async sendSubscriptionConfirmation(@Body() user: User): Promise<void | ErrorResponse> {
|
||||
return await this.mailService.sendSubscriptionConfirmation(user);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('send2Friend')
|
||||
async send2Friend(@Body() shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
|
||||
return await this.mailService.send2Friend(shareByEMail);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { join } from 'path';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
@@ -10,40 +11,13 @@ import { UserModule } from '../user/user.module';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { MailController } from './mail.controller';
|
||||
import { MailService } from './mail.service';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DrizzleModule,
|
||||
UserModule,
|
||||
GeoModule,
|
||||
// ConfigModule.forFeature(mailConfig),
|
||||
// MailerModule.forRoot({
|
||||
// transport: {
|
||||
// host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
// secure: false,
|
||||
// port: 587,
|
||||
// auth: {
|
||||
// user: user, //'AKIAU6GDWVAQ2QNFLNWN',
|
||||
// pass: password, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
// },
|
||||
// },
|
||||
// defaults: {
|
||||
// from: '"No Reply" <noreply@example.com>',
|
||||
// },
|
||||
// template: {
|
||||
// dir: join(__dirname, 'templates'),
|
||||
// adapter: new HandlebarsAdapter({
|
||||
// eq: function (a, b) {
|
||||
// return a === b;
|
||||
// },
|
||||
// }),
|
||||
// options: {
|
||||
// strict: true,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
FirebaseAdminModule,
|
||||
MailerModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
transport: {
|
||||
@@ -51,8 +25,8 @@ import { MailService } from './mail.service';
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.AMAZON_USER, //'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: process.env.AMAZON_PASSWORD, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
user: process.env.AMAZON_USER,
|
||||
pass: process.env.AMAZON_PASSWORD,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { getAuth } from 'firebase-admin/auth';
|
||||
import { join } from 'path';
|
||||
import { ZodError } from 'zod';
|
||||
import { SenderSchema, ShareByEMail, ShareByEMailSchema, User } from '../models/db.model';
|
||||
@@ -52,6 +53,65 @@ export class MailService {
|
||||
},
|
||||
});
|
||||
}
|
||||
async sendVerificationEmail(
|
||||
email: string,
|
||||
redirectConfig: { protocol: string, hostname: string, port?: number }
|
||||
): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
// Firebase Auth-Instanz holen
|
||||
const auth = getAuth();
|
||||
|
||||
// Baue den Redirect-URL aus den übergebenen Parametern
|
||||
let continueUrl = `${redirectConfig.protocol}://${redirectConfig.hostname}`;
|
||||
if (redirectConfig.port) {
|
||||
continueUrl += `:${redirectConfig.port}`;
|
||||
}
|
||||
continueUrl += '/auth/verify-email-success'; // Beispiel für einen Weiterleitungspfad
|
||||
|
||||
// Custom Verification Link generieren
|
||||
const firebaseActionLink = await auth.generateEmailVerificationLink(email, {
|
||||
url: continueUrl,
|
||||
handleCodeInApp: false,
|
||||
});
|
||||
|
||||
// Extrahiere den oobCode aus dem Firebase Link
|
||||
const actionLinkUrl = new URL(firebaseActionLink);
|
||||
const oobCode = actionLinkUrl.searchParams.get('oobCode');
|
||||
|
||||
if (!oobCode) {
|
||||
throw new BadRequestException('Failed to generate verification code');
|
||||
}
|
||||
|
||||
// Erstelle die benutzerdefinierte URL mit dem oobCode
|
||||
let customActionLink = `${redirectConfig.protocol}://${redirectConfig.hostname}`;
|
||||
if (redirectConfig.port) {
|
||||
customActionLink += `:${redirectConfig.port}`;
|
||||
}
|
||||
|
||||
// Ersetze die Platzhalter mit den tatsächlichen Werten
|
||||
customActionLink += `/email-authorized?email=${encodeURIComponent(email)}&mode=verifyEmail&oobCode=${oobCode}`;
|
||||
|
||||
// Zufallszahl für die E-Mail generieren
|
||||
const randomNumber = Math.floor(Math.random() * 10000);
|
||||
|
||||
// E-Mail senden
|
||||
await this.mailerService.sendMail({
|
||||
to: email,
|
||||
from: '"Bizmatch Team" <info@bizmatch.net>',
|
||||
subject: 'Verify your email address',
|
||||
template: join(__dirname, '../..', 'mail/templates/email-verification.hbs'),
|
||||
context: {
|
||||
actionLink: customActionLink,
|
||||
randomNumber: randomNumber
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Error sending verification email:', error);
|
||||
throw new BadRequestException('Failed to send verification email');
|
||||
}
|
||||
}
|
||||
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
SenderSchema.parse(mailInfo.sender);
|
||||
|
||||
249
bizmatch-server/src/mail/templates/email-verification.hbs
Normal file
249
bizmatch-server/src/mail/templates/email-verification.hbs
Normal file
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"> <!-- utf-8 works for most cases -->
|
||||
<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
|
||||
<meta name="x-apple-disable-message-reformatting"> <!-- Disable auto-scale in iOS 10 Mail entirely -->
|
||||
<title>Email address verification</title> <!-- The title tag shows in email notifications, like Android 4.4. -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700" rel="stylesheet">
|
||||
|
||||
<!-- CSS Reset : BEGIN -->
|
||||
<style>
|
||||
/* What it does: Remove spaces around the email design added by some email clients. */
|
||||
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
|
||||
html,
|
||||
body {
|
||||
margin: 0 auto !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
background: #f1f1f1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* What it does: Stops email clients resizing small text. */
|
||||
* {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* What it does: Centers email on Android 4.4 */
|
||||
div[style*="margin: 16px 0"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* What it does: Stops Outlook from adding extra spacing to tables. */
|
||||
table,
|
||||
td {
|
||||
mso-table-lspace: 0pt !important;
|
||||
mso-table-rspace: 0pt !important;
|
||||
}
|
||||
|
||||
/* What it does: Fixes webkit padding issue. */
|
||||
table {
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: collapse !important;
|
||||
table-layout: fixed !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* What it does: Uses a better rendering method when resizing images in IE. */
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* What it does: A work-around for email clients meddling in triggered links. */
|
||||
*[x-apple-data-detectors],
|
||||
/* iOS */
|
||||
.unstyle-auto-detected-links *,
|
||||
.aBn {
|
||||
border-bottom: 0 !important;
|
||||
cursor: default !important;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
/* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
|
||||
.a6S {
|
||||
display: none !important;
|
||||
opacity: 0.01 !important;
|
||||
}
|
||||
|
||||
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
|
||||
.im {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* If the above doesn't work, add a .g-img class to any image in question. */
|
||||
img.g-img+div {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
|
||||
/* Create one of these media queries for each additional viewport size you'd like to fix */
|
||||
|
||||
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
|
||||
u~div .email-container {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 6, 6S, 7, 8, and X */
|
||||
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
|
||||
u~div .email-container {
|
||||
min-width: 375px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 6+, 7+, and 8+ */
|
||||
@media only screen and (min-device-width: 414px) {
|
||||
u~div .email-container {
|
||||
min-width: 414px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- CSS Reset : END -->
|
||||
|
||||
<!-- Progressive Enhancements : BEGIN -->
|
||||
<style>
|
||||
.primary {
|
||||
background: #30e3ca;
|
||||
}
|
||||
|
||||
.bg_white {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.bg_light {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.bg_black {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.bg_dark {
|
||||
background: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.email-section {
|
||||
padding: 2.5em;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
/*HERO*/
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero .text {
|
||||
color: rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
.hero .text h2 {
|
||||
color: #000;
|
||||
font-size: 40px;
|
||||
margin-bottom: 0;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hero .text h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.hero .text h2 span {
|
||||
font-weight: 600;
|
||||
color: #30e3ca;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
display: block;
|
||||
color: black;
|
||||
line-height: 32px;
|
||||
font-weight: 300;
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, sans-serif;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@media (max-width:400px) {
|
||||
.hero img {
|
||||
width: 200px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body width="100%"
|
||||
style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f1f1f1; display: flex; align-items: center; justify-content: center;">
|
||||
<div style="width: 100%; background-color: #f1f1f1;">
|
||||
<div
|
||||
style="display: none; font-size: 1px;max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
|
||||
Hello, click on the button below to verify your email address
|
||||
</div>
|
||||
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
|
||||
<!-- BEGIN BODY -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
<tr>
|
||||
<td valign="middle" class="hero bg_white" style="padding: 3em 0 2em 0;">
|
||||
<img src="https://github.com/ColorlibHQ/email-templates/blob/master/10/images/email.png?raw=true"
|
||||
alt="" class="g-img" style="width: 200px; height: auto; margin: auto; display: block;">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- end tr -->
|
||||
<tr>
|
||||
<td valign="middle" class="hero bg_white" style="padding: 2em 0 4em 0;">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="text" style="padding: 0 2.5em; text-align: center;">
|
||||
<h2 style="margin-bottom: 20px; font-size: 32px;">Verify your email address</h2>
|
||||
<p class="email-body">
|
||||
Thanks for signup with us. Click on the button below to verify your email
|
||||
address.
|
||||
</p>
|
||||
<a href="{{actionLink}}" target="_blank"
|
||||
style="padding:15px 40px; background-color: #5D91E8; color: white;">Verify
|
||||
your email</a>
|
||||
<p class="email-body">
|
||||
If this email wasn't intended for you feel free to delete it.<br />
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- end tr -->
|
||||
<span style="color: #f1f1f1; display: none;">{{randomNumber}}</span>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import bodyParser from 'body-parser';
|
||||
import express from 'express';
|
||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||
import { AppModule } from './app.module';
|
||||
@@ -12,7 +11,7 @@ async function bootstrap() {
|
||||
// const logger = app.get<Logger>(WINSTON_MODULE_NEST_PROVIDER);
|
||||
const logger = app.get<LoggerService>(WINSTON_MODULE_NEST_PROVIDER);
|
||||
app.useLogger(logger);
|
||||
app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
|
||||
//app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
|
||||
app.setGlobalPrefix('bizmatch');
|
||||
|
||||
app.enableCors({
|
||||
|
||||
@@ -186,6 +186,7 @@ export const UserSchema = z
|
||||
updated: z.date().optional().nullable(),
|
||||
subscriptionId: z.string().optional().nullable(),
|
||||
subscriptionPlan: SubscriptionTypeEnum.optional().nullable(),
|
||||
showInDirectory: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.customerType === 'professional') {
|
||||
@@ -233,7 +234,7 @@ export const UserSchema = z
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company location is required for professional customers',
|
||||
path: ['companyLocation'],
|
||||
path: ['location'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Param, Post, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Get, HttpException, HttpStatus, Param, Post, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { AdminAuthGuard } from 'src/jwt-auth/admin-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { Checkout } from 'src/models/main.model';
|
||||
import Stripe from 'stripe';
|
||||
import { PaymentService } from './payment.service';
|
||||
@@ -15,25 +14,25 @@ export class PaymentController {
|
||||
// return this.paymentService.createSubscription(subscriptionData);
|
||||
// }
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('user/all')
|
||||
async getAllStripeCustomer(): Promise<Stripe.Customer[]> {
|
||||
return await this.paymentService.getAllStripeCustomer();
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Get('user/all')
|
||||
// async getAllStripeCustomer(): Promise<Stripe.Customer[]> {
|
||||
// return await this.paymentService.getAllStripeCustomer();
|
||||
// }
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('subscription/all')
|
||||
async getAllStripeSubscriptions(): Promise<Stripe.Subscription[]> {
|
||||
return await this.paymentService.getAllStripeSubscriptions();
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Get('subscription/all')
|
||||
// async getAllStripeSubscriptions(): Promise<Stripe.Subscription[]> {
|
||||
// return await this.paymentService.getAllStripeSubscriptions();
|
||||
// }
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('paymentmethod/:email')
|
||||
async getStripePaymentMethods(@Param('email') email: string): Promise<Stripe.PaymentMethod[]> {
|
||||
return await this.paymentService.getStripePaymentMethod(email);
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Get('paymentmethod/:email')
|
||||
// async getStripePaymentMethods(@Param('email') email: string): Promise<Stripe.PaymentMethod[]> {
|
||||
// return await this.paymentService.getStripePaymentMethod(email);
|
||||
// }
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('create-checkout-session')
|
||||
async createCheckoutSession(@Body() checkout: Checkout) {
|
||||
return await this.paymentService.createCheckoutSession(checkout);
|
||||
@@ -59,7 +58,7 @@ export class PaymentController {
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('subscriptions/:email')
|
||||
async findSubscriptionsById(@Param('email') email: string): Promise<any> {
|
||||
return await this.paymentService.getSubscription(email);
|
||||
@@ -68,10 +67,10 @@ export class PaymentController {
|
||||
* Endpoint zum Löschen eines Stripe-Kunden.
|
||||
* Beispiel: DELETE /stripe/customer/cus_12345
|
||||
*/
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Delete('customer/:id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async deleteCustomer(@Param('id') customerId: string): Promise<void> {
|
||||
await this.paymentService.deleteCustomerCompletely(customerId);
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Delete('customer/:id')
|
||||
// @HttpCode(HttpStatus.NO_CONTENT)
|
||||
// async deleteCustomer(@Param('id') customerId: string): Promise<void> {
|
||||
// await this.paymentService.deleteCustomerCompletely(customerId);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
@@ -12,8 +13,8 @@ import { PaymentController } from './payment.controller';
|
||||
import { PaymentService } from './payment.service';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, UserModule, MailModule, AuthModule],
|
||||
providers: [PaymentService, UserService, MailService, FileService, GeoService, AuthService],
|
||||
imports: [DrizzleModule, UserModule, MailModule, AuthModule,FirebaseAdminModule],
|
||||
providers: [PaymentService, UserService, MailService, FileService, GeoService],
|
||||
controllers: [PaymentController],
|
||||
})
|
||||
export class PaymentModule {}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import Stripe from 'stripe';
|
||||
import { Logger } from 'winston';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { PG_CONNECTION } from '../drizzle/schema';
|
||||
import { MailService } from '../mail/mail.service';
|
||||
@@ -22,7 +21,6 @@ export class PaymentService {
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private readonly userService: UserService,
|
||||
private readonly mailService: MailService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-06-20',
|
||||
@@ -91,28 +89,28 @@ export class PaymentService {
|
||||
return this.stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
|
||||
}
|
||||
async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise<void> {
|
||||
try {
|
||||
const keycloakUsers = await this.authService.getUsers();
|
||||
const keycloakUser = keycloakUsers.find(u => u.email === session.customer_details.email);
|
||||
const user = await this.userService.getUserByMail(session.customer_details.email, {
|
||||
userId: keycloakUser.id,
|
||||
firstname: keycloakUser.firstName,
|
||||
lastname: keycloakUser.lastName,
|
||||
username: keycloakUser.email,
|
||||
roles: [],
|
||||
});
|
||||
user.subscriptionId = session.subscription as string;
|
||||
const subscription = await this.stripe.subscriptions.retrieve(user.subscriptionId);
|
||||
user.customerType = 'professional';
|
||||
if (subscription.metadata['plan'] === 'Broker Plan') {
|
||||
user.customerSubType = 'broker';
|
||||
}
|
||||
user.subscriptionPlan = subscription.metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
|
||||
await this.userService.saveUser(user, false);
|
||||
await this.mailService.sendSubscriptionConfirmation(user);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
// try {
|
||||
// const keycloakUsers = await this.authService.getUsers();
|
||||
// const keycloakUser = keycloakUsers.find(u => u.email === session.customer_details.email);
|
||||
// const user = await this.userService.getUserByMail(session.customer_details.email, {
|
||||
// userId: keycloakUser.id,
|
||||
// firstname: keycloakUser.firstName,
|
||||
// lastname: keycloakUser.lastName,
|
||||
// username: keycloakUser.email,
|
||||
// roles: [],
|
||||
// });
|
||||
// user.subscriptionId = session.subscription as string;
|
||||
// const subscription = await this.stripe.subscriptions.retrieve(user.subscriptionId);
|
||||
// user.customerType = 'professional';
|
||||
// if (subscription.metadata['plan'] === 'Broker Plan') {
|
||||
// user.customerSubType = 'broker';
|
||||
// }
|
||||
// user.subscriptionPlan = subscription.metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
|
||||
// await this.userService.saveUser(user, false);
|
||||
// await this.mailService.sendSubscriptionConfirmation(user);
|
||||
// } catch (error) {
|
||||
// this.logger.error(error);
|
||||
// }
|
||||
}
|
||||
async getSubscription(email: string): Promise<Stripe.Subscription[]> {
|
||||
const existingCustomers = await this.stripe.customers.list({
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { SelectOptionsService } from './select-options.service';
|
||||
|
||||
@Controller('select-options')
|
||||
export class SelectOptionsController {
|
||||
constructor(private selectOptionsService: SelectOptionsService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get()
|
||||
getSelectOption(): any {
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FirebaseAdminModule } from '../firebase-admin/firebase-admin.module';
|
||||
import { SelectOptionsController } from './select-options.controller';
|
||||
import { SelectOptionsService } from './select-options.service';
|
||||
|
||||
@Module({
|
||||
imports: [FirebaseAdminModule],
|
||||
controllers: [SelectOptionsController],
|
||||
providers: [SelectOptionsService],
|
||||
})
|
||||
|
||||
27
bizmatch-server/src/setup-admin.command.ts
Normal file
27
bizmatch-server/src/setup-admin.command.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { BadRequestException, Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { AdminAuthGuard } from 'src/jwt-auth/admin-auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { ZodError } from 'zod';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { JwtAuthGuard } from '../jwt-auth/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
|
||||
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';
|
||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model';
|
||||
import { UserService } from './user.service';
|
||||
@@ -18,26 +19,26 @@ export class UserController {
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get()
|
||||
async findByMail(@Request() req, @Query('mail') mail: string): Promise<User> {
|
||||
const user = await this.userService.getUserByMail(mail, req.user as JwtUser);
|
||||
return user;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
async findById(@Param('id') id: string): Promise<User> {
|
||||
const user = await this.userService.getUserById(id);
|
||||
return user;
|
||||
}
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('user/all')
|
||||
async getAllUser(): Promise<User[]> {
|
||||
return await this.userService.getAllUser();
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
async save(@Body() user: any): Promise<User> {
|
||||
try {
|
||||
@@ -57,27 +58,27 @@ export class UserController {
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('guaranteed')
|
||||
async saveGuaranteed(@Body() user: any): Promise<User> {
|
||||
const savedUser = await this.userService.saveUser(user, false);
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('search')
|
||||
async find(@Body() criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
|
||||
const foundUsers = await this.userService.searchUserListings(criteria);
|
||||
return foundUsers;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('findTotal')
|
||||
async findTotal(@Body() criteria: UserListingCriteria): Promise<number> {
|
||||
return await this.userService.getUserListingsCount(criteria);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('subscriptions/:id')
|
||||
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
|
||||
const subscriptions = [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
@@ -7,7 +8,7 @@ import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, GeoModule],
|
||||
imports: [DrizzleModule, GeoModule,FirebaseAdminModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService, FileService, GeoService],
|
||||
})
|
||||
|
||||
@@ -52,6 +52,10 @@ export class UserService {
|
||||
if (criteria.state) {
|
||||
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`);
|
||||
}
|
||||
|
||||
//never show user which denied
|
||||
whereConditions.push(eq(schema.users.showInDirectory, true))
|
||||
|
||||
return whereConditions;
|
||||
}
|
||||
async searchUserListings(criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
|
||||
@@ -106,7 +110,7 @@ export class UserService {
|
||||
.from(schema.users)
|
||||
.where(sql`email = ${email}`)) as User[];
|
||||
if (users.length === 0) {
|
||||
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname, null) };
|
||||
const user: User = { id: undefined, customerType: 'professional', ...createDefaultUser(email, jwtuser.firstname ? jwtuser.firstname : '', jwtuser.lastname ? jwtuser.lastname : '', null) };
|
||||
const u = await this.saveUser(user, false);
|
||||
return u;
|
||||
} else {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@angular/common": "^18.1.3",
|
||||
"@angular/compiler": "^18.1.3",
|
||||
"@angular/core": "^18.1.3",
|
||||
"@angular/fire": "^18.0.1",
|
||||
"@angular/forms": "^18.1.3",
|
||||
"@angular/platform-browser": "^18.1.3",
|
||||
"@angular/platform-browser-dynamic": "^18.1.3",
|
||||
@@ -25,11 +26,10 @@
|
||||
"@angular/router": "^18.1.3",
|
||||
"@bluehalo/ngx-leaflet": "^18.0.2",
|
||||
"@fortawesome/angular-fontawesome": "^0.15.0",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@ng-select/ng-select": "^13.4.1",
|
||||
"@ngneat/until-destroy": "^10.0.0",
|
||||
"@stripe/stripe-js": "^4.3.0",
|
||||
@@ -41,8 +41,6 @@
|
||||
"express": "^4.18.2",
|
||||
"flowbite": "^2.4.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"keycloak-angular": "^16.0.1",
|
||||
"keycloak-js": "^25.0.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"memoize-one": "^6.0.0",
|
||||
"ng-gallery": "^11.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"/api": {
|
||||
"/bizmatch": {
|
||||
"target": "http://localhost:3000",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- <div class="container"> -->
|
||||
<div class="flex flex-col" [ngClass]="{ 'bg-slate-100 print:bg-white': actualRoute !== 'home' }">
|
||||
@if (actualRoute !=='home'){
|
||||
<div class="wrapper" [ngClass]="{ 'print:bg-white': actualRoute !== 'home' }">
|
||||
@if (actualRoute !=='home' && actualRoute !=='login' && actualRoute!=='emailVerification' && actualRoute!=='email-authorized'){
|
||||
<header></header>
|
||||
}
|
||||
<main class="flex-grow">
|
||||
<main class="flex-1 bg-slate-100">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||
import { KeycloakEventType, KeycloakService } from 'keycloak-angular';
|
||||
|
||||
import { filter } from 'rxjs/operators';
|
||||
import build from '../build';
|
||||
@@ -34,7 +33,6 @@ export class AppComponent {
|
||||
public loadingService: LoadingService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private keycloakService: KeycloakService,
|
||||
private userService: UserService,
|
||||
private confirmationService: ConfirmationService,
|
||||
private auditService: AuditService,
|
||||
@@ -50,35 +48,33 @@ export class AppComponent {
|
||||
});
|
||||
}
|
||||
ngOnInit() {
|
||||
// Überwache Keycloak-Events, um den Token-Refresh zu kontrollieren
|
||||
this.keycloakService.keycloakEvents$.subscribe({
|
||||
next: event => {
|
||||
if (event.type === KeycloakEventType.OnTokenExpired) {
|
||||
// Wenn der Token abgelaufen ist, versuchen wir einen Refresh
|
||||
this.handleTokenExpiration();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
private async handleTokenExpiration(): Promise<void> {
|
||||
try {
|
||||
// Versuche, den Token zu erneuern
|
||||
const refreshed = await this.keycloakService.updateToken();
|
||||
if (!refreshed) {
|
||||
// Wenn der Token nicht erneuert werden kann, leite zur Login-Seite weiter
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href, // oder eine andere Seite
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.error === 'invalid_grant' && error.error_description === 'Token is not active') {
|
||||
// Hier wird der Fehler "invalid_grant" abgefangen
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href,
|
||||
});
|
||||
}
|
||||
}
|
||||
// this.keycloakService.keycloakEvents$.subscribe({
|
||||
// next: event => {
|
||||
// if (event.type === KeycloakEventType.OnTokenExpired) {
|
||||
// this.handleTokenExpiration();
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
}
|
||||
// private async handleTokenExpiration(): Promise<void> {
|
||||
// try {
|
||||
// // Versuche, den Token zu erneuern
|
||||
// const refreshed = await this.keycloakService.updateToken();
|
||||
// if (!refreshed) {
|
||||
// // Wenn der Token nicht erneuert werden kann, leite zur Login-Seite weiter
|
||||
// this.keycloakService.login({
|
||||
// redirectUri: window.location.href, // oder eine andere Seite
|
||||
// });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// if (error.error === 'invalid_grant' && error.error_description === 'Token is not active') {
|
||||
// // Hier wird der Fehler "invalid_grant" abgefangen
|
||||
// this.keycloakService.login({
|
||||
// redirectUri: window.location.href,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
|
||||
|
||||
@@ -2,18 +2,20 @@ import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core'
|
||||
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
|
||||
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
|
||||
import { getAuth, provideAuth } from '@angular/fire/auth';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
|
||||
import { GALLERY_CONFIG, GalleryConfig } from 'ng-gallery';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
import { provideNgxStripe } from 'ngx-stripe';
|
||||
import { environment } from '../environments/environment';
|
||||
import { routes } from './app.routes';
|
||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
||||
import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
|
||||
import { GlobalErrorHandler } from './services/globalErrorHandler';
|
||||
import { KeycloakInitializerService } from './services/keycloak-initializer.service';
|
||||
import { SelectOptionsService } from './services/select-options.service';
|
||||
import { createLogger } from './utils/utils';
|
||||
// provideClientHydration()
|
||||
@@ -21,16 +23,6 @@ const logger = createLogger('ApplicationConfig');
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{ provide: KeycloakService },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
// useFactory: initializeKeycloak1,
|
||||
//useFactory: initializeKeycloak2,
|
||||
useFactory: initializeKeycloak,
|
||||
multi: true,
|
||||
//deps: [KeycloakService],
|
||||
deps: [KeycloakInitializerService],
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initServices,
|
||||
@@ -42,16 +34,12 @@ export const appConfig: ApplicationConfig = {
|
||||
useClass: LoadingInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: TimeoutInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
||||
{
|
||||
provide: 'TIMEOUT_DURATION',
|
||||
useValue: 5000, // Standard-Timeout von 5 Sekunden
|
||||
@@ -93,6 +81,9 @@ export const appConfig: ApplicationConfig = {
|
||||
],
|
||||
},
|
||||
}),
|
||||
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
|
||||
provideAuth(() => getAuth()),
|
||||
// provideFirestore(() => getFirestore()),
|
||||
],
|
||||
};
|
||||
function initServices(selectOptions: SelectOptionsService) {
|
||||
@@ -100,47 +91,3 @@ function initServices(selectOptions: SelectOptionsService) {
|
||||
await selectOptions.init();
|
||||
};
|
||||
}
|
||||
export function initializeKeycloak(keycloak: KeycloakInitializerService) {
|
||||
return () => keycloak.initialize();
|
||||
}
|
||||
|
||||
// export function initializeKeycloak1(keycloak: KeycloakService): () => Promise<void> {
|
||||
// return async () => {
|
||||
// const { url, realm, clientId } = environment.keycloak;
|
||||
// const adapter = customKeycloakAdapter(() => keycloak.getKeycloakInstance(), {});
|
||||
// if (window.location.search.length > 0) {
|
||||
// sessionStorage.setItem('SEARCH', window.location.search);
|
||||
// }
|
||||
// const { host, hostname, href, origin, pathname, port, protocol, search } = window.location;
|
||||
// await keycloak.init({
|
||||
// config: { url, realm, clientId },
|
||||
// initOptions: {
|
||||
// onLoad: 'check-sso',
|
||||
// silentCheckSsoRedirectUri: window.location.hostname === 'localhost' ? `${window.location.origin}/assets/silent-check-sso.html` : `${window.location.origin}/dealerweb/assets/silent-check-sso.html`,
|
||||
// adapter,
|
||||
// redirectUri: `${origin}${pathname}`,
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
// }
|
||||
// function initializeKeycloak2(keycloak: KeycloakService) {
|
||||
// return async () => {
|
||||
// logger.info(`###>calling keycloakService init ...`);
|
||||
// const authenticated = await keycloak.init({
|
||||
// config: {
|
||||
// url: environment.keycloak.url,
|
||||
// realm: environment.keycloak.realm,
|
||||
// clientId: environment.keycloak.clientId,
|
||||
// },
|
||||
// initOptions: {
|
||||
// onLoad: 'check-sso',
|
||||
// silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
// },
|
||||
// bearerExcludedUrls: ['/assets'],
|
||||
// shouldUpdateToken(request) {
|
||||
// return !request.headers.get('token-update') === false;
|
||||
// },
|
||||
// });
|
||||
// logger.info(`+++>${authenticated}`);
|
||||
// };
|
||||
// }
|
||||
|
||||
@@ -2,6 +2,9 @@ import { Routes } from '@angular/router';
|
||||
import { LogoutComponent } from './components/logout/logout.component';
|
||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
|
||||
import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component';
|
||||
import { EmailVerificationComponent } from './components/email-verification/email-verification.component';
|
||||
import { LoginRegisterComponent } from './components/login-register/login-register.component';
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
||||
import { UserListComponent } from './pages/admin/user-list/user-list.component';
|
||||
@@ -12,7 +15,6 @@ import { HomeComponent } from './pages/home/home.component';
|
||||
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
|
||||
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
|
||||
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
|
||||
import { LoginComponent } from './pages/login/login.component';
|
||||
import { PricingComponent } from './pages/pricing/pricing.component';
|
||||
import { AccountComponent } from './pages/subscription/account/account.component';
|
||||
import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component';
|
||||
@@ -57,9 +59,17 @@ export const routes: Routes = [
|
||||
canActivate: [ListingCategoryGuard],
|
||||
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
},
|
||||
// {
|
||||
// path: 'login/:page',
|
||||
// component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
// },
|
||||
{
|
||||
path: 'login/:page',
|
||||
component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
},
|
||||
{
|
||||
path: 'notfound',
|
||||
@@ -139,6 +149,14 @@ export const routes: Routes = [
|
||||
path: 'pricing',
|
||||
component: PricingComponent,
|
||||
},
|
||||
{
|
||||
path: 'emailVerification',
|
||||
component: EmailVerificationComponent,
|
||||
},
|
||||
{
|
||||
path: 'email-authorized',
|
||||
component: EmailAuthorizedComponent,
|
||||
},
|
||||
{
|
||||
path: 'pricingOverview',
|
||||
component: PricingComponent,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="container mx-auto p-4 text-center min-h-screen bg-gray-100">
|
||||
<ng-container *ngIf="verificationStatus === 'pending'">
|
||||
<p class="text-lg text-gray-600">Verifying your email...</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="verificationStatus === 'success'">
|
||||
<h2 class="text-2xl font-bold text-green-600 mb-5">Your email has been verified</h2>
|
||||
<!-- <p class="text-gray-700 mb-4">You can now sign in with your new account</p> -->
|
||||
<a routerLink="/account" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">Follow this link to access your Account Page </a>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="verificationStatus === 'error'">
|
||||
<h2 class="text-2xl font-bold text-red-600 mb-2">Verification failed</h2>
|
||||
<p class="text-gray-700">{{ errorMessage }}</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-email-authorized',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule],
|
||||
templateUrl: './email-authorized.component.html',
|
||||
})
|
||||
export class EmailAuthorizedComponent implements OnInit {
|
||||
verificationStatus: 'pending' | 'success' | 'error' = 'pending';
|
||||
errorMessage: string | null = null;
|
||||
|
||||
constructor(private route: ActivatedRoute, private http: HttpClient, private authService: AuthService, private userService: UserService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const oobCode = this.route.snapshot.queryParamMap.get('oobCode');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
const mode = this.route.snapshot.queryParamMap.get('mode');
|
||||
|
||||
if (mode === 'verifyEmail' && oobCode && email) {
|
||||
this.verifyEmail(oobCode, email);
|
||||
} else {
|
||||
this.verificationStatus = 'error';
|
||||
this.errorMessage = 'Invalid verification link';
|
||||
}
|
||||
}
|
||||
|
||||
private verifyEmail(oobCode: string, email: string): void {
|
||||
this.http.post(`${environment.apiBaseUrl}/bizmatch/auth/verify-email`, { oobCode, email }).subscribe({
|
||||
next: async () => {
|
||||
this.verificationStatus = 'success';
|
||||
await this.authService.refreshToken();
|
||||
const user = await this.userService.getByMail(email);
|
||||
},
|
||||
error: err => {
|
||||
this.verificationStatus = 'error';
|
||||
this.errorMessage = err.error?.message || 'Verification failed';
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
|
||||
<div class="bg-white p-8 rounded shadow-md w-full max-w-md text-center">
|
||||
<h2 class="text-2xl font-bold mb-4">Email Verification</h2>
|
||||
<p class="mb-4">A verification email has been sent to your email address. Please check your inbox and click the link to verify your account.</p>
|
||||
<p>Once verified, please return to the application.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-email-verification',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './email-verification.component.html',
|
||||
})
|
||||
export class EmailVerificationComponent {}
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="flex flex-col lg:flex-row items-center order-3 lg:order-2">
|
||||
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" data-drawer-target="terms-of-use" data-drawer-show="terms-of-use" aria-controls="terms-of-use">Terms of use</a>
|
||||
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" data-drawer-target="privacy" data-drawer-show="privacy" aria-controls="privacy">Privacy statement</a>
|
||||
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" routerLink="/pricingOverview">Pricing</a>
|
||||
<!-- <a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" routerLink="/pricingOverview">Pricing</a> -->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row items-center order-2 lg:order-3">
|
||||
|
||||
@@ -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>
|
||||
@@ -111,6 +111,7 @@
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
@@ -120,6 +121,7 @@
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
@@ -129,16 +131,17 @@
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-unknown">
|
||||
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||
<li>
|
||||
<a (click)="login()" 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">Log In</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" 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">Log In</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/pricing" 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">Register</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" 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">Register</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-2 md:hidden">
|
||||
@@ -151,6 +154,7 @@
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
@@ -160,6 +164,7 @@
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
@@ -169,6 +174,7 @@
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -200,6 +206,7 @@
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
@@ -210,6 +217,7 @@
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
@@ -220,6 +228,7 @@
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,13 @@ import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { filter, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, KeyValueAsSortBy, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { CriteriaChangeService } from '../../services/criteria-change.service';
|
||||
import { ListingsService } from '../../services/listings.service';
|
||||
import { SearchService } from '../../services/search.service';
|
||||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { SharedService } from '../../services/shared.service';
|
||||
@@ -46,8 +47,9 @@ export class HeaderComponent {
|
||||
baseRoute: string;
|
||||
sortDropdownVisible: boolean;
|
||||
sortByOptions: KeyValueAsSortBy[] = [];
|
||||
numberOfBroker$: Observable<number>;
|
||||
numberOfCommercial$: Observable<number>;
|
||||
constructor(
|
||||
public keycloakService: KeycloakService,
|
||||
private router: Router,
|
||||
private userService: UserService,
|
||||
private sharedService: SharedService,
|
||||
@@ -56,6 +58,8 @@ export class HeaderComponent {
|
||||
private searchService: SearchService,
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
public selectOptions: SelectOptionsService,
|
||||
public authService: AuthService,
|
||||
private listingService: ListingsService,
|
||||
) {}
|
||||
@HostListener('document:click', ['$event'])
|
||||
handleGlobalClick(event: Event) {
|
||||
@@ -65,13 +69,14 @@ export class HeaderComponent {
|
||||
}
|
||||
}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.keycloakUser) {
|
||||
this.user = await this.userService.getByMail(this.keycloakUser?.email);
|
||||
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');
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 10);
|
||||
@@ -124,11 +129,6 @@ export class HeaderComponent {
|
||||
navigateWithState(dest: string, state: any) {
|
||||
this.router.navigate([dest], { state: state });
|
||||
}
|
||||
login() {
|
||||
this.keycloakService.login({
|
||||
redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`,
|
||||
});
|
||||
}
|
||||
|
||||
isActive(route: string): boolean {
|
||||
return this.router.url === route;
|
||||
@@ -189,9 +189,7 @@ export class HeaderComponent {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
isAdmin() {
|
||||
return this.keycloakService.getUserRoles(true).includes('ADMIN');
|
||||
}
|
||||
|
||||
sortBy(sortBy: SortByOptions) {
|
||||
this.criteria.sortBy = sortBy;
|
||||
this.sortDropdownVisible = false;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
|
||||
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
|
||||
<h2 class="text-2xl font-bold mb-6 text-center text-gray-800">
|
||||
{{ isLoginMode ? 'Login' : 'Sign Up' }}
|
||||
</h2>
|
||||
|
||||
<!-- Toggle Switch mit Flowbite -->
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
<span class="mr-3 text-gray-700 font-medium">Login</span>
|
||||
<label for="toggle-switch" class="inline-flex relative items-center cursor-pointer">
|
||||
<input type="checkbox" id="toggle-switch" class="sr-only peer" [checked]="!isLoginMode" (change)="toggleMode()" />
|
||||
<div
|
||||
class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:bg-gray-700 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"
|
||||
></div>
|
||||
</label>
|
||||
<span class="ml-3 text-gray-700 font-medium">Sign Up</span>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Eingabe -->
|
||||
<div class="mb-4">
|
||||
<label for="email" class="block text-gray-700 mb-2 font-medium">E-Mail</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
[(ngModel)]="email"
|
||||
placeholder="Please enter E-Mail Address"
|
||||
class="w-full px-3 py-2 pl-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<fa-icon [icon]="envelope" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passwort Eingabe -->
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block text-gray-700 mb-2 font-medium">Password</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
[(ngModel)]="password"
|
||||
placeholder="Please enter Password"
|
||||
class="w-full px-3 py-2 pl-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passwort-Bestätigung nur im Registrierungsmodus -->
|
||||
<div *ngIf="!isLoginMode" class="mb-6">
|
||||
<label for="confirmPassword" class="block text-gray-700 mb-2 font-medium">Confirm Password</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
[(ngModel)]="confirmPassword"
|
||||
placeholder="Repeat Password"
|
||||
class="w-full px-3 py-2 pl-10 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<fa-icon [icon]="lock" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fehlermeldung -->
|
||||
<div *ngIf="errorMessage" class="text-red-500 text-center mb-4 text-sm">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button (click)="onSubmit()" class="w-full flex items-center justify-center bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-lg mb-4 transition-colors duration-200">
|
||||
<!-- <fa-icon [icon]="isLoginMode ? 'fas fas-user-plus' : 'userplus'" class="mr-2"></fa-icon> -->
|
||||
<i *ngIf="isLoginMode" class="fa-solid fa-user-plus mr-2"></i>
|
||||
<i *ngIf="!isLoginMode" class="fa-solid fa-arrow-right mr-2"></i>
|
||||
{{ isLoginMode ? 'Sign in with Email' : 'Register' }}
|
||||
</button>
|
||||
|
||||
<!-- Trennlinie -->
|
||||
<div class="flex items-center justify-center my-4">
|
||||
<span class="border-b w-1/5 md:w-1/4 border-gray-300"></span>
|
||||
<span class="text-xs text-gray-500 uppercase mx-2">or</span>
|
||||
<span class="border-b w-1/5 md:w-1/4 border-gray-300"></span>
|
||||
</div>
|
||||
|
||||
<!-- Google Button -->
|
||||
<button (click)="loginWithGoogle()" class="w-full flex items-center justify-center bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 py-2.5 rounded-lg transition-colors duration-200">
|
||||
<!-- <svg class="h-5 w-5 mr-2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.24 10.32V13.8H15.48C15.336 14.688 14.568 16.368 12.24 16.368C10.224 16.368 8.568 14.688 8.568 12.672C8.568 10.656 10.224 8.976 12.24 8.976C13.32 8.976 14.16 9.432 14.688 10.08L16.704 8.208C15.528 7.032 14.04 6.384 12.24 6.384C8.832 6.384 6 9.216 6 12.672C6 16.128 8.832 18.96 12.24 18.96C15.696 18.96 18.12 16.656 18.12 12.672C18.12 11.952 18.024 11.28 17.88 10.656L12.24 10.32Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg> -->
|
||||
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||
<path
|
||||
fill="#FFC107"
|
||||
d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"
|
||||
/>
|
||||
<path fill="#FF3D00" d="M6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z" />
|
||||
<path fill="#4CAF50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0124 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z" />
|
||||
<path fill="#1976D2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 01-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z" />
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
<!-- <button (click)="loginWithGoogle()" class="bg-white text-blue-600 px-6 py-3 rounded-lg shadow-lg hover:bg-gray-100 transition duration-300 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||
<path
|
||||
fill="#FFC107"
|
||||
d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"
|
||||
/>
|
||||
<path fill="#FF3D00" d="M6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z" />
|
||||
<path fill="#4CAF50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0124 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z" />
|
||||
<path fill="#1976D2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 01-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z" />
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,95 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { faArrowRight, faEnvelope, faLock, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { LoadingService } from '../../services/loading.service';
|
||||
@Component({
|
||||
selector: 'app-login-register',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, FontAwesomeModule],
|
||||
templateUrl: './login-register.component.html',
|
||||
})
|
||||
export class LoginRegisterComponent {
|
||||
email: string = '';
|
||||
password: string = '';
|
||||
confirmPassword: string = '';
|
||||
isLoginMode: boolean = true; // true: Login, false: Registration
|
||||
errorMessage: string = '';
|
||||
envelope = faEnvelope;
|
||||
lock = faLock;
|
||||
arrowRight = faArrowRight;
|
||||
userplus = faUserPlus;
|
||||
constructor(private authService: AuthService, private route: ActivatedRoute, private router: Router, private loadingService: LoadingService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Set mode based on query parameter "mode"
|
||||
this.route.queryParamMap.subscribe(params => {
|
||||
const mode = params.get('mode');
|
||||
this.isLoginMode = mode !== 'register';
|
||||
});
|
||||
}
|
||||
|
||||
toggleMode(): void {
|
||||
this.isLoginMode = !this.isLoginMode;
|
||||
this.errorMessage = '';
|
||||
}
|
||||
|
||||
// Login with Email
|
||||
onSubmit(): void {
|
||||
this.errorMessage = '';
|
||||
if (this.isLoginMode) {
|
||||
this.authService
|
||||
.loginWithEmail(this.email, this.password)
|
||||
.then(userCredential => {
|
||||
console.log('Successfully logged in:', userCredential);
|
||||
this.router.navigate([`home`]);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during email login:', error);
|
||||
this.errorMessage = error.message;
|
||||
});
|
||||
} else {
|
||||
// Registration mode: also check if passwords match
|
||||
if (this.password !== this.confirmPassword) {
|
||||
console.error('Passwords do not match');
|
||||
this.errorMessage = 'Passwords do not match.';
|
||||
return;
|
||||
}
|
||||
this.loadingService.startLoading('googleAuth');
|
||||
this.authService
|
||||
.registerWithEmail(this.email, this.password)
|
||||
.then(userCredential => {
|
||||
console.log('Successfully registered:', userCredential);
|
||||
this.loadingService.stopLoading('googleAuth');
|
||||
this.router.navigate(['emailVerification']);
|
||||
})
|
||||
.catch(error => {
|
||||
this.loadingService.stopLoading('googleAuth');
|
||||
console.error('Error during registration:', error);
|
||||
if (error.code === 'auth/email-already-in-use') {
|
||||
this.errorMessage = 'This email address is already in use. Please try logging in.';
|
||||
} else {
|
||||
this.errorMessage = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Login with Google
|
||||
loginWithGoogle(): void {
|
||||
this.errorMessage = '';
|
||||
this.authService
|
||||
.loginWithGoogle()
|
||||
.then(userCredential => {
|
||||
console.log('Successfully logged in with Google:', userCredential);
|
||||
this.router.navigate([`home`]);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during Google login:', error);
|
||||
this.errorMessage = error.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'logout',
|
||||
@@ -10,8 +11,8 @@ import { KeycloakService } from 'keycloak-angular';
|
||||
template: ``,
|
||||
})
|
||||
export class LogoutComponent {
|
||||
constructor(public keycloakService: KeycloakService) {
|
||||
sessionStorage.removeItem('USERID');
|
||||
keycloakService.logout(window.location.origin + '/home');
|
||||
constructor(private authService: AuthService, private router: Router) {
|
||||
this.authService.logout();
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
|
||||
import { KeycloakInitializerService } from '../services/keycloak-initializer.service';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { createLogger } from '../utils/utils';
|
||||
const logger = createLogger('AuthGuard');
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuard extends KeycloakAuthGuard {
|
||||
constructor(protected override readonly router: Router, protected readonly keycloak: KeycloakService, private keycloakInitializer: KeycloakInitializerService) {
|
||||
super(router, keycloak);
|
||||
}
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private authService: AuthService, private router: Router) {}
|
||||
|
||||
async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree> {
|
||||
logger.info(`--->AuthGuard`);
|
||||
while (!this.keycloakInitializer.initialized) {
|
||||
logger.info(`Waiting 100 msec`);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
// Force the user to log in if currently unauthenticated.
|
||||
const authenticated = this.keycloak.isLoggedIn();
|
||||
//this.keycloak.isTokenExpired()
|
||||
if (!this.authenticated && !authenticated) {
|
||||
await this.keycloak.login({
|
||||
redirectUri: window.location.origin + state.url,
|
||||
});
|
||||
// return false;
|
||||
}
|
||||
|
||||
// Get the roles required from the route.
|
||||
const requiredRoles = route.data['roles'];
|
||||
|
||||
// Allow the user to proceed if no additional roles are required to access the route.
|
||||
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
|
||||
async canActivate(): Promise<boolean> {
|
||||
const token = await this.authService.getToken();
|
||||
if (token) {
|
||||
return true;
|
||||
} else {
|
||||
this.router.navigate(['/login-register']);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow the user to proceed if all the required roles are present.
|
||||
return requiredRoles.every(role => this.roles.includes(role));
|
||||
}
|
||||
}
|
||||
|
||||
34
bizmatch/src/app/interceptors/auth.interceptor.ts
Normal file
34
bizmatch/src/app/interceptors/auth.interceptor.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, from } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
// Prüfe, ob die Anfrage an die apiBaseUrl gerichtet ist
|
||||
const isApiRequest = req.url.startsWith(environment.apiBaseUrl);
|
||||
|
||||
if (!isApiRequest) {
|
||||
// Wenn es keine API-Anfrage ist, leite die Anfrage unverändert weiter
|
||||
return next.handle(req);
|
||||
}
|
||||
|
||||
// Wenn es eine API-Anfrage ist, füge den Token hinzu (falls vorhanden)
|
||||
return from(this.authService.getToken()).pipe(
|
||||
switchMap(token => {
|
||||
if (token) {
|
||||
const clonedReq = req.clone({
|
||||
setHeaders: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
return next.handle(clonedReq);
|
||||
}
|
||||
return next.handle(req);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
<!-- 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" />
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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()"
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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
|
||||
}"
|
||||
>
|
||||
<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
|
||||
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"
|
||||
></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>
|
||||
</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>
|
||||
<!-- 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 *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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-template #noCCInfo>
|
||||
<p>Keine Kreditkarteninformationen verfügbar.</p>
|
||||
</ng-template>
|
||||
</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>
|
||||
<!-- Modal-Fuß -->
|
||||
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
</div>
|
||||
|
||||
<!-- "Mehr laden"-Button -->
|
||||
<div *ngIf="hasMoreUsers" class="flex justify-center mt-6 mb-8">
|
||||
<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()"
|
||||
(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"
|
||||
>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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);
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
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);
|
||||
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>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { LeafletModule } from '@bluehalo/ngx-leaflet';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { BusinessListing, EventTypeEnum, ShareByEMail, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
@@ -25,6 +24,7 @@ import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { createMailInfo, map2User } from '../../../utils/utils';
|
||||
// Import für Leaflet
|
||||
// Benannte Importe für Leaflet
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { BaseDetailsComponent } from '../base-details.component';
|
||||
@Component({
|
||||
selector: 'app-details-business-listing',
|
||||
@@ -74,12 +74,12 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
private mailService: MailService,
|
||||
private sanitizer: DomSanitizer,
|
||||
public historyService: HistoryService,
|
||||
public keycloakService: KeycloakService,
|
||||
private validationMessagesService: ValidationMessagesService,
|
||||
private messageService: MessageService,
|
||||
private auditService: AuditService,
|
||||
public emailService: EMailService,
|
||||
private geoService: GeoService,
|
||||
public authService: AuthService,
|
||||
) {
|
||||
super();
|
||||
this.router.events.subscribe(event => {
|
||||
@@ -92,7 +92,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.keycloakUser) {
|
||||
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||
@@ -115,9 +115,7 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
ngOnDestroy() {
|
||||
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
|
||||
}
|
||||
isAdmin() {
|
||||
return 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>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { LeafletModule } from '@bluehalo/ngx-leaflet';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { GalleryModule, ImageItem } from 'ng-gallery';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
@@ -17,6 +16,7 @@ import { ValidatedNgSelectComponent } from '../../../components/validated-ng-sel
|
||||
import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component';
|
||||
import { ValidationMessagesService } from '../../../components/validation-messages.service';
|
||||
import { AuditService } from '../../../services/audit.service';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { HistoryService } from '../../../services/history.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
@@ -77,20 +77,20 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
private mailService: MailService,
|
||||
private sanitizer: DomSanitizer,
|
||||
public historyService: HistoryService,
|
||||
public keycloakService: KeycloakService,
|
||||
private imageService: ImageService,
|
||||
private ngZone: NgZone,
|
||||
private validationMessagesService: ValidationMessagesService,
|
||||
private messageService: MessageService,
|
||||
private auditService: AuditService,
|
||||
private emailService: EMailService,
|
||||
public authService: AuthService,
|
||||
) {
|
||||
super();
|
||||
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.keycloakUser) {
|
||||
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||
@@ -139,9 +139,6 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
.catch(error => console.error('Error initializing Flowbite:', error));
|
||||
});
|
||||
}
|
||||
isAdmin() {
|
||||
return this.keycloakService.getUserRoles(true).includes('ADMIN');
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { KeycloakUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { HistoryService } from '../../../services/history.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
@@ -45,7 +45,7 @@ export class DetailsUserComponent {
|
||||
private sanitizer: DomSanitizer,
|
||||
private imageService: ImageService,
|
||||
public historyService: HistoryService,
|
||||
public keycloakService: KeycloakService,
|
||||
public authService: AuthService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -54,14 +54,9 @@ export class DetailsUserComponent {
|
||||
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
|
||||
this.businessListings = results[0];
|
||||
this.commercialPropListings = results[1] as CommercialPropertyListing[];
|
||||
//this.user$ = this.userService.getUserObservable();
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview ? this.user.companyOverview : '');
|
||||
this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices ? this.user.offeredServices : '');
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
return this.keycloakService.getUserRoles(true).includes('ADMIN');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<header class="w-full flex justify-between items-center p-4 bg-white fixed top-0 z-10 h-16 md:h-20">
|
||||
<header class="w-full flex justify-between items-center p-4 bg-white top-0 z-10 h-16 md:h-20">
|
||||
<img src="assets/images/header-logo.png" alt="Logo" class="h-8 md:h-10" />
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
@if(user){
|
||||
<a routerLink="/account" class="text-blue-600 border border-blue-600 px-3 py-2 rounded">Account</a>
|
||||
} @else {
|
||||
<!-- <a routerLink="/pricing" class="text-gray-800">Pricing</a> -->
|
||||
<a (click)="login()" class="text-blue-600 border border-blue-600 px-3 py-2 rounded">Log In</a>
|
||||
<a routerLink="/pricing" class="text-white bg-blue-600 px-4 py-2 rounded">Register</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="text-blue-600 border border-blue-600 px-3 py-2 rounded">Log In</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="text-white bg-blue-600 px-4 py-2 rounded">Register</a>
|
||||
<!-- <a routerLink="/login" class="text-blue-500 hover:underline">Login/Register</a> -->
|
||||
}
|
||||
</div>
|
||||
<button (click)="toggleMenu()" class="md:hidden text-gray-600">
|
||||
@@ -22,8 +23,8 @@
|
||||
@if(user){
|
||||
<a routerLink="/account" class="text-white text-xl py-2">Account</a>
|
||||
} @else {
|
||||
<a (click)="login()" class="text-white text-xl py-2">Log In</a>
|
||||
<a routerLink="/pricing" class="text-white text-xl py-2">Register</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="text-white text-xl py-2">Log In</a>
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'register' }" class="text-white text-xl py-2">Register</a>
|
||||
}
|
||||
<button (click)="toggleMenu()" class="text-white mt-4">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -33,13 +34,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="flex flex-col items-center justify-center mt-16 md:mt-20 lg:px-4 w-full flex-grow">
|
||||
<main class="flex flex-col items-center justify-center lg:px-4 w-full flex-grow">
|
||||
<div class="bg-cover-custom py-20 md:py-40 flex flex-col w-full">
|
||||
<div class="flex justify-center w-full">
|
||||
<div class="w-11/12 md:w-2/3 lg:w-1/2">
|
||||
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-blue-900 mb-4 text-center">Find businesses for sale.</h1>
|
||||
<p class="text-base md:text-lg lg:text-xl text-blue-600 mb-8 text-center">Unlocking Exclusive Opportunities - Empowering Entrepreneurial Dreams</p>
|
||||
<div class="bg-white bg-opacity-80 p-2 rounded-lg shadow-lg w-full" [ngClass]="{ 'pt-6': aiSearch }">
|
||||
<div class="bg-white bg-opacity-80 pb-6 pt-2 px-2 rounded-lg shadow-lg w-full" [ngClass]="{ 'pt-6': aiSearch }">
|
||||
@if(!aiSearch){
|
||||
<div class="text-sm lg:text-base mb-1 text-center text-gray-500 border-gray-200 dark:text-gray-400 dark:border-gray-700 flex justify-center">
|
||||
<ul class="flex flex-wrap -mb-px">
|
||||
@@ -55,6 +56,7 @@
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li class="me-2">
|
||||
<a
|
||||
(click)="changeTab('commercialProperty')"
|
||||
@@ -67,6 +69,8 @@
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
@if ((numberOfBroker$ | async) > 0) {
|
||||
<li class="me-2">
|
||||
<a
|
||||
(click)="changeTab('broker')"
|
||||
@@ -79,6 +83,7 @@
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
} @if(aiSearch){
|
||||
@@ -188,7 +193,7 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="mt-4 flex items-center justify-center text-gray-700">
|
||||
<!-- <div class="mt-4 flex items-center justify-center text-gray-700">
|
||||
<span class="mr-2">AI-Search</span>
|
||||
<span [attr.data-tooltip-target]="tooltipTargetBeta" class="bg-sky-300 text-teal-800 text-xs font-semibold px-2 py-1 rounded">BETA</span>
|
||||
<app-tooltip [id]="tooltipTargetBeta" text="AI will convert your input into filter criteria. Please check them in the filter menu after search"></app-tooltip>
|
||||
@@ -197,7 +202,7 @@
|
||||
<input (click)="toggleAiSearch()" type="checkbox" name="toggle" id="toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 border-gray-300 appearance-none cursor-pointer" />
|
||||
<label for="toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,12 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { catchError, concat, debounceTime, distinctUntilChanged, lastValueFrom, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs';
|
||||
import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { ModalService } from '../../components/search-modal/modal.service';
|
||||
import { TooltipComponent } from '../../components/tooltip/tooltip.component';
|
||||
import { AiService } from '../../services/ai.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { CriteriaChangeService } from '../../services/criteria-change.service';
|
||||
import { GeoService } from '../../services/geo.service';
|
||||
import { ListingsService } from '../../services/listings.service';
|
||||
@@ -52,7 +52,8 @@ export class HomeComponent {
|
||||
cityOrState = undefined;
|
||||
private criteriaChangeSubscription: Subscription;
|
||||
numberOfResults$: Observable<number>;
|
||||
|
||||
numberOfBroker$: Observable<number>;
|
||||
numberOfCommercial$: Observable<number>;
|
||||
aiSearch = false;
|
||||
aiSearchText = '';
|
||||
aiSearchFailed = false;
|
||||
@@ -71,19 +72,22 @@ export class HomeComponent {
|
||||
private searchService: SearchService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
public selectOptions: SelectOptionsService,
|
||||
public keycloakService: KeycloakService,
|
||||
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
private geoService: GeoService,
|
||||
public cdRef: ChangeDetectorRef,
|
||||
private listingService: ListingsService,
|
||||
private userService: UserService,
|
||||
private aiService: AiService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 0);
|
||||
const token = await this.keycloakService.getToken();
|
||||
this.numberOfBroker$ = this.userService.getNumberOfBroker(createEmptyUserListingCriteria());
|
||||
this.numberOfCommercial$ = this.listingService.getNumberOfListings(createEmptyCommercialPropertyListingCriteria(), 'commercialProperty');
|
||||
const token = await this.authService.getToken();
|
||||
sessionStorage.removeItem('businessListings');
|
||||
sessionStorage.removeItem('commercialPropertyListings');
|
||||
sessionStorage.removeItem('brokerListings');
|
||||
@@ -112,14 +116,7 @@ export class HomeComponent {
|
||||
private setupCriteriaChangeListener() {
|
||||
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(untilDestroyed(this), debounceTime(400)).subscribe(() => this.setTotalNumberOfResults());
|
||||
}
|
||||
login() {
|
||||
this.keycloakService.login({
|
||||
redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`,
|
||||
});
|
||||
}
|
||||
register() {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||
}
|
||||
|
||||
toggleMenu() {
|
||||
this.isMenuOpen = !this.isMenuOpen;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { SubscriptionsService } from '../../services/subscriptions.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { map2User } from '../../utils/utils';
|
||||
@@ -15,9 +16,16 @@ import { map2User } from '../../utils/utils';
|
||||
})
|
||||
export class LoginComponent {
|
||||
page: string | undefined = this.activatedRoute.snapshot.params['page'] as string | undefined;
|
||||
constructor(public userService: UserService, private activatedRoute: ActivatedRoute, private keycloakService: KeycloakService, private router: Router, private subscriptionService: SubscriptionsService) {}
|
||||
constructor(
|
||||
public userService: UserService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
|
||||
private router: Router,
|
||||
private subscriptionService: SubscriptionsService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
const email = keycloakUser.email;
|
||||
const user = await this.userService.getByMail(email);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { StripeService } from 'ngx-stripe';
|
||||
import { switchMap } from 'rxjs';
|
||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { Checkout, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AuditService } from '../../services/audit.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { SharedModule } from '../../shared/shared/shared.module';
|
||||
import { map2User } from '../../utils/utils';
|
||||
@@ -26,17 +26,17 @@ export class PricingComponent {
|
||||
keycloakUser: KeycloakUser;
|
||||
user: User;
|
||||
constructor(
|
||||
public keycloakService: KeycloakService,
|
||||
private http: HttpClient,
|
||||
private stripeService: StripeService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private userService: UserService,
|
||||
private router: Router,
|
||||
private auditService: AuditService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.keycloakUser) {
|
||||
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||
@@ -56,19 +56,6 @@ export class PricingComponent {
|
||||
this.checkout({ priceId: atob(base64PriceId), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||
}
|
||||
}
|
||||
// if (this.id === 'free' || this.keycloakUser.priceId === 'free') {
|
||||
// this.user.subscriptionPlan = 'free';
|
||||
// await this.userService.saveGuaranteed(this.user);
|
||||
// this.router.navigate([`/account`]);
|
||||
// } else if (this.id || this.keycloakUser.priceId) {
|
||||
// const priceId = this.id ? this.id : this.keycloakUser.priceId;
|
||||
// this.checkout({ priceId: atob(priceId), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||
// } else if (!this.id && !this.pricingOverview) {
|
||||
// this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||
// if (this.user.subscriptionId) {
|
||||
// this.router.navigate([`/account`]);
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
this.pricingOverview = false;
|
||||
}
|
||||
@@ -85,13 +72,13 @@ export class PricingComponent {
|
||||
this.checkout({ priceId: priceId, email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||
}
|
||||
} else {
|
||||
if (priceId) {
|
||||
this.keycloakService.register({
|
||||
redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}`,
|
||||
});
|
||||
} else {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/free` });
|
||||
}
|
||||
// if (priceId) {
|
||||
// this.keycloakService.register({
|
||||
// redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}`,
|
||||
// });
|
||||
// } else {
|
||||
// this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/free` });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,14 +71,14 @@
|
||||
<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>
|
||||
</div>
|
||||
|
||||
}@else{
|
||||
<app-validated-select [disabled]="user.customerType === 'professional'" label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
|
||||
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
|
||||
} @if (isProfessional){
|
||||
<!-- <div>
|
||||
<label for="customerSubType" class="block text-sm font-medium text-gray-700">Professional Type</label>
|
||||
@@ -86,7 +86,7 @@
|
||||
<option *ngFor="let subType of customerSubTypes" [value]="subType">{{ subType | titlecase }}</option>
|
||||
</select>
|
||||
</div> -->
|
||||
<app-validated-select [disabled]="user.customerSubType === 'broker'" label="Professional Type" name="customerSubType" [(ngModel)]="user.customerSubType" [options]="customerSubTypeOptions"></app-validated-select>
|
||||
<app-validated-select label="Professional Type" name="customerSubType" [(ngModel)]="user.customerSubType" [options]="customerSubTypeOptions"></app-validated-select>
|
||||
}
|
||||
</div>
|
||||
@if (isProfessional){
|
||||
@@ -220,13 +220,23 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="flex items-center !my-8">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<div class="relative">
|
||||
<input type="checkbox" [(ngModel)]="user.showInDirectory" name="showInDirectory" class="hidden" />
|
||||
<div class="toggle-bg block w-12 h-6 rounded-full bg-gray-600 transition"></div>
|
||||
</div>
|
||||
<div class="ml-3 text-gray-700 font-medium">Show your profile in Professional Directory</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start">
|
||||
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" (click)="updateProfile(user)">
|
||||
Update Profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-8 max-lg:hidden">
|
||||
<!-- <div class="mt-8 max-lg:hidden">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-2">Membership Level</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full">
|
||||
@@ -290,8 +300,8 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if(user.subscriptionPlan==='free'){
|
||||
</div> -->
|
||||
<!-- @if(user.subscriptionPlan==='free'){
|
||||
<div class="flex justify-start">
|
||||
<button
|
||||
routerLink="/pricing"
|
||||
@@ -300,7 +310,7 @@
|
||||
Upgrade Subscription Plan
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
} -->
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,13 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
|
||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||
import { ImageCropperComponent } from 'ngx-image-cropper';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AutoCompleteCompleteEvent, Invoice, StripeSubscription, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { AutoCompleteCompleteEvent, Invoice, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||
@@ -25,15 +25,15 @@ import { ValidatedLocationComponent } from '../../../components/validated-locati
|
||||
import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component';
|
||||
import { ValidatedSelectComponent } from '../../../components/validated-select/validated-select.component';
|
||||
import { ValidationMessagesService } from '../../../components/validation-messages.service';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { GeoService } from '../../../services/geo.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { LoadingService } from '../../../services/loading.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { SharedService } from '../../../services/shared.service';
|
||||
import { SubscriptionsService } from '../../../services/subscriptions.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { checkAndUpdate, map2User } from '../../../utils/utils';
|
||||
import { map2User } from '../../../utils/utils';
|
||||
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
@@ -78,7 +78,7 @@ export class AccountComponent {
|
||||
customerSubTypeOptions: Array<{ value: string; label: string }> = [];
|
||||
tooltipTargetAreasServed = 'tooltip-areasServed';
|
||||
tooltipTargetLicensed = 'tooltip-licensedIn';
|
||||
subscriptions: StripeSubscription[] | any[];
|
||||
// subscriptions: StripeSubscription[] | any[];
|
||||
constructor(
|
||||
public userService: UserService,
|
||||
private geoService: GeoService,
|
||||
@@ -88,15 +88,15 @@ export class AccountComponent {
|
||||
private loadingService: LoadingService,
|
||||
private imageUploadService: ImageService,
|
||||
private imageService: ImageService,
|
||||
private keycloakService: KeycloakService,
|
||||
private confirmationService: ConfirmationService,
|
||||
private messageService: MessageService,
|
||||
private sharedService: SharedService,
|
||||
private titleCasePipe: TitleCasePipe,
|
||||
private validationMessagesService: ValidationMessagesService,
|
||||
private subscriptionService: SubscriptionsService,
|
||||
// private subscriptionService: SubscriptionsService,
|
||||
private datePipe: DatePipe,
|
||||
private router: Router,
|
||||
public authService: AuthService,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
setTimeout(() => {
|
||||
@@ -105,63 +105,63 @@ export class AccountComponent {
|
||||
if (this.id) {
|
||||
this.user = await this.userService.getById(this.id);
|
||||
} else {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
const email = keycloakUser.email;
|
||||
this.user = await this.userService.getByMail(email);
|
||||
}
|
||||
|
||||
this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email));
|
||||
await this.synchronizeSubscriptions(this.subscriptions);
|
||||
// this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email));
|
||||
// await this.synchronizeSubscriptions(this.subscriptions);
|
||||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||
|
||||
this.customerTypeOptions = this.selectOptions.customerTypes
|
||||
.filter(ct => ct.value === 'buyer' || ct.value === 'seller' || this.user.customerType === 'professional')
|
||||
// .filter(ct => ct.value === 'buyer' || ct.value === 'seller' || this.user.customerType === 'professional')
|
||||
.map(type => ({
|
||||
value: type.value,
|
||||
label: this.titleCasePipe.transform(type.name),
|
||||
}));
|
||||
|
||||
this.customerSubTypeOptions = this.selectOptions.customerSubTypes
|
||||
.filter(ct => ct.value !== 'broker' || this.user.customerSubType === 'broker')
|
||||
// .filter(ct => ct.value !== 'broker' || this.user.customerSubType === 'broker')
|
||||
.map(type => ({
|
||||
value: type.value,
|
||||
label: this.titleCasePipe.transform(type.name),
|
||||
}));
|
||||
}
|
||||
async synchronizeSubscriptions(subscriptions: StripeSubscription[]) {
|
||||
let changed = false;
|
||||
if (this.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
if (this.subscriptions.length === 0) {
|
||||
if (!this.user.subscriptionPlan) {
|
||||
this.router.navigate(['pricing']);
|
||||
} else {
|
||||
this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }];
|
||||
changed = checkAndUpdate(changed, this.user.customerType !== 'buyer' && this.user.customerType !== 'seller', () => (this.user.customerType = 'buyer'));
|
||||
changed = checkAndUpdate(changed, !!this.user.customerSubType, () => (this.user.customerSubType = null));
|
||||
changed = checkAndUpdate(changed, this.user.subscriptionPlan !== 'free', () => (this.user.subscriptionPlan = 'free'));
|
||||
changed = checkAndUpdate(changed, !!this.user.subscriptionId, () => (this.user.subscriptionId = null));
|
||||
}
|
||||
} else {
|
||||
const subscription = subscriptions[0];
|
||||
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.customerType !== 'professional', () => (this.user.customerType = 'professional'));
|
||||
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.customerSubType !== 'broker', () => (this.user.customerSubType = 'broker'));
|
||||
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.subscriptionPlan !== 'broker', () => (this.user.subscriptionPlan = 'broker'));
|
||||
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && !this.user.subscriptionId, () => (this.user.subscriptionId = subscription.id));
|
||||
// async synchronizeSubscriptions(subscriptions: StripeSubscription[]) {
|
||||
// let changed = false;
|
||||
// if (this.isAdmin()) {
|
||||
// return;
|
||||
// }
|
||||
// if (this.subscriptions.length === 0) {
|
||||
// if (!this.user.subscriptionPlan) {
|
||||
// this.router.navigate(['pricing']);
|
||||
// } else {
|
||||
// this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }];
|
||||
// changed = checkAndUpdate(changed, this.user.customerType !== 'buyer' && this.user.customerType !== 'seller', () => (this.user.customerType = 'buyer'));
|
||||
// changed = checkAndUpdate(changed, !!this.user.customerSubType, () => (this.user.customerSubType = null));
|
||||
// changed = checkAndUpdate(changed, this.user.subscriptionPlan !== 'free', () => (this.user.subscriptionPlan = 'free'));
|
||||
// changed = checkAndUpdate(changed, !!this.user.subscriptionId, () => (this.user.subscriptionId = null));
|
||||
// }
|
||||
// } else {
|
||||
// const subscription = subscriptions[0];
|
||||
// changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.customerType !== 'professional', () => (this.user.customerType = 'professional'));
|
||||
// changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.customerSubType !== 'broker', () => (this.user.customerSubType = 'broker'));
|
||||
// changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && this.user.subscriptionPlan !== 'broker', () => (this.user.subscriptionPlan = 'broker'));
|
||||
// changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Broker Plan' && !this.user.subscriptionId, () => (this.user.subscriptionId = subscription.id));
|
||||
|
||||
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.customerType !== 'professional', () => (this.user.customerType = 'professional'));
|
||||
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.subscriptionPlan !== 'professional', () => (this.user.subscriptionPlan = 'professional'));
|
||||
changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.subscriptionId !== 'professional', () => (this.user.subscriptionId = subscription.id));
|
||||
}
|
||||
if (changed) {
|
||||
await this.userService.saveGuaranteed(this.user);
|
||||
this.cdref.detectChanges();
|
||||
this.cdref.markForCheck();
|
||||
}
|
||||
}
|
||||
// changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.customerType !== 'professional', () => (this.user.customerType = 'professional'));
|
||||
// changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.subscriptionPlan !== 'professional', () => (this.user.subscriptionPlan = 'professional'));
|
||||
// changed = checkAndUpdate(changed, subscription.metadata['plan'] === 'Professional Plan' && this.user.subscriptionId !== 'professional', () => (this.user.subscriptionId = subscription.id));
|
||||
// }
|
||||
// if (changed) {
|
||||
// await this.userService.saveGuaranteed(this.user);
|
||||
// this.cdref.detectChanges();
|
||||
// this.cdref.markForCheck();
|
||||
// }
|
||||
// }
|
||||
|
||||
ngOnDestroy() {
|
||||
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
|
||||
@@ -264,27 +264,25 @@ export class AccountComponent {
|
||||
const message = this.validationMessages.find(msg => msg.field === fieldName);
|
||||
return message ? message.message : '';
|
||||
}
|
||||
isAdmin() {
|
||||
return this.keycloakService.getUserRoles(true).includes('ADMIN');
|
||||
}
|
||||
|
||||
setState(index: number, state: string) {
|
||||
if (state === null) {
|
||||
this.user.areasServed[index].county = null;
|
||||
}
|
||||
}
|
||||
getLevel(i: number) {
|
||||
return this.subscriptions[i].metadata.plan;
|
||||
}
|
||||
getStartDate(i: number) {
|
||||
return this.datePipe.transform(new Date(this.subscriptions[i].start_date * 1000));
|
||||
}
|
||||
getEndDate(i: number) {
|
||||
return this.subscriptions[i].status === 'trialing' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||
}
|
||||
getNextSettlement(i: number) {
|
||||
return this.subscriptions[i].status === 'active' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||
}
|
||||
getStatus(i: number) {
|
||||
return this.subscriptions[i].status ? this.subscriptions[i].status : '';
|
||||
}
|
||||
// getLevel(i: number) {
|
||||
// return this.subscriptions[i].metadata.plan;
|
||||
// }
|
||||
// getStartDate(i: number) {
|
||||
// return this.datePipe.transform(new Date(this.subscriptions[i].start_date * 1000));
|
||||
// }
|
||||
// getEndDate(i: number) {
|
||||
// return this.subscriptions[i].status === 'trialing' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||
// }
|
||||
// getNextSettlement(i: number) {
|
||||
// return this.subscriptions[i].status === 'active' ? this.datePipe.transform(new Date(this.subscriptions[i].current_period_end * 1000)) : '---';
|
||||
// }
|
||||
// getStatus(i: number) {
|
||||
// return this.subscriptions[i].status ? this.subscriptions[i].status : '';
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { map2User, routeListingWithState } from '../../../utils/utils';
|
||||
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
@@ -26,6 +25,7 @@ import { ValidatedQuillComponent } from '../../../components/validated-quill/val
|
||||
import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component';
|
||||
import { ValidationMessagesService } from '../../../components/validation-messages.service';
|
||||
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { GeoService } from '../../../services/geo.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { LoadingService } from '../../../services/loading.service';
|
||||
@@ -85,8 +85,8 @@ export class EditBusinessListingComponent {
|
||||
private loadingService: LoadingService,
|
||||
private messageService: MessageService,
|
||||
private route: ActivatedRoute,
|
||||
private keycloakService: KeycloakService,
|
||||
private validationMessagesService: ValidationMessagesService,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
this.router.events.subscribe(event => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
@@ -103,7 +103,7 @@ export class EditBusinessListingComponent {
|
||||
});
|
||||
}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
this.listingUser = await this.userService.getByMail(keycloakUser.email);
|
||||
if (this.mode === 'edit') {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ViewportRuler } from '@angular/cdk/scrolling';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { NgxCurrencyDirective } from 'ngx-currency';
|
||||
import { ImageCropperComponent } from 'ngx-image-cropper';
|
||||
import { QuillModule } from 'ngx-quill';
|
||||
@@ -30,6 +29,7 @@ import { ValidatedPriceComponent } from '../../../components/validated-price/val
|
||||
import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component';
|
||||
import { ValidationMessagesService } from '../../../components/validation-messages.service';
|
||||
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { GeoService } from '../../../services/geo.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { LoadingService } from '../../../services/loading.service';
|
||||
@@ -122,12 +122,12 @@ export class EditCommercialPropertyListingComponent {
|
||||
private loadingService: LoadingService,
|
||||
|
||||
private route: ActivatedRoute,
|
||||
private keycloakService: KeycloakService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private confirmationService: ConfirmationService,
|
||||
private messageService: MessageService,
|
||||
private viewportRuler: ViewportRuler,
|
||||
private validationMessagesService: ValidationMessagesService,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
// Abonniere Router-Events, um den aktiven Link zu ermitteln
|
||||
this.router.events.subscribe(event => {
|
||||
@@ -145,7 +145,7 @@ export class EditCommercialPropertyListingComponent {
|
||||
});
|
||||
}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
const email = keycloakUser.email;
|
||||
this.user = await this.userService.getByMail(email);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
|
||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { MessageService } from '../../../components/message/message.service';
|
||||
@@ -9,6 +7,7 @@ import { ValidatedNgSelectComponent } from '../../../components/validated-ng-sel
|
||||
import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component';
|
||||
import { ValidationMessagesService } from '../../../components/validation-messages.service';
|
||||
import { AuditService } from '../../../services/audit.service';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { MailService } from '../../../services/mail.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
@@ -30,16 +29,16 @@ export class EmailUsComponent {
|
||||
constructor(
|
||||
private mailService: MailService,
|
||||
private userService: UserService,
|
||||
public keycloakService: KeycloakService,
|
||||
private validationMessagesService: ValidationMessagesService,
|
||||
private messageService: MessageService,
|
||||
public selectOptions: SelectOptionsService,
|
||||
private auditService: AuditService,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
this.mailinfo = createMailInfo();
|
||||
}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.keycloakUser) {
|
||||
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { BusinessListing, CommercialPropertyListing } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { KeycloakUser } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
@@ -20,9 +20,9 @@ export class FavoritesComponent {
|
||||
user: KeycloakUser;
|
||||
// listings: Array<ListingType> = []; //= dataListings as unknown as Array<BusinessListing>;
|
||||
favorites: Array<BusinessListing | CommercialPropertyListing>;
|
||||
constructor(public keycloakService: KeycloakService, private listingsService: ListingsService, public selectOptions: SelectOptionsService, private confirmationService: ConfirmationService) {}
|
||||
constructor(private listingsService: ListingsService, public selectOptions: SelectOptionsService, private confirmationService: ConfirmationService, private authService: AuthService) {}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
this.user = map2User(token);
|
||||
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]);
|
||||
this.favorites = [...result[0], ...result[1]];
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||
import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
|
||||
import { MessageComponent } from '../../../components/message/message.component';
|
||||
import { MessageService } from '../../../components/message/message.service';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
@@ -26,16 +26,15 @@ export class MyListingComponent {
|
||||
user: User;
|
||||
constructor(
|
||||
public userService: UserService,
|
||||
public keycloakService: KeycloakService,
|
||||
private listingsService: ListingsService,
|
||||
private cdRef: ChangeDetectorRef,
|
||||
public selectOptions: SelectOptionsService,
|
||||
private messageService: MessageService,
|
||||
private confirmationService: ConfirmationService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
// const keycloakUser = this.userService.getKeycloakUser();
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
const email = keycloakUser.email;
|
||||
this.user = await this.userService.getByMail(email);
|
||||
@@ -59,14 +58,5 @@ export class MyListingComponent {
|
||||
// this.messageService.showMessage('Listing has been deleted');
|
||||
this.deleteListing(listing);
|
||||
}
|
||||
// this.confirmationService.confirm({
|
||||
// target: event.target as EventTarget,
|
||||
// message: 'Are you sure you want to delet this listing?',
|
||||
// icon: 'pi pi-exclamation-triangle',
|
||||
// accept: () => {
|
||||
// this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been deleted', life: 3000 });
|
||||
// this.deleteListing(listing);
|
||||
// },
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { AuditService } from '../../services/audit.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { map2User } from '../../utils/utils';
|
||||
|
||||
@@ -18,12 +18,12 @@ export class SuccessComponent {
|
||||
user: User;
|
||||
maxAttemptsReached: boolean = false; // Neue Variable hinzufügen
|
||||
|
||||
constructor(private keycloakService: KeycloakService, private userService: UserService, private auditService: AuditService, private router: Router) {}
|
||||
constructor(private userService: UserService, private auditService: AuditService, private router: Router, private authService: AuthService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
let email = null;
|
||||
try {
|
||||
const token = await this.keycloakService.getToken();
|
||||
const token = await this.authService.getToken();
|
||||
const keycloakUser = map2User(token);
|
||||
email = keycloakUser.email;
|
||||
this.user = await this.userService.getByMail(email);
|
||||
|
||||
277
bizmatch/src/app/services/auth.service.ts
Normal file
277
bizmatch/src/app/services/auth.service.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
// auth.service.ts
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { FirebaseApp } from '@angular/fire/app';
|
||||
import { GoogleAuthProvider, UserCredential, createUserWithEmailAndPassword, getAuth, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth';
|
||||
import { BehaviorSubject, Observable, catchError, firstValueFrom, map, of, shareReplay, take, tap } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { MailService } from './mail.service';
|
||||
|
||||
export type UserRole = 'admin' | 'pro' | 'guest';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
private app = inject(FirebaseApp);
|
||||
private auth = getAuth(this.app);
|
||||
private http = inject(HttpClient);
|
||||
private mailService = inject(MailService);
|
||||
// Add a BehaviorSubject to track the current user role
|
||||
private userRoleSubject = new BehaviorSubject<UserRole | null>(null);
|
||||
public userRole$ = this.userRoleSubject.asObservable();
|
||||
// Referenz für den gecachten API-Aufruf
|
||||
private cachedUserRole$: Observable<UserRole | null> | null = null;
|
||||
|
||||
// Zeitraum in ms, nach dem der Cache zurückgesetzt werden soll (z.B. 5 Minuten)
|
||||
private cacheDuration = 5 * 60 * 1000;
|
||||
private lastCacheTime = 0;
|
||||
constructor() {
|
||||
// Load role from token when service is initialized
|
||||
this.loadRoleFromToken();
|
||||
}
|
||||
|
||||
private loadRoleFromToken(): void {
|
||||
this.getToken().then(token => {
|
||||
if (token) {
|
||||
const role = this.extractRoleFromToken(token);
|
||||
this.userRoleSubject.next(role);
|
||||
} else {
|
||||
this.userRoleSubject.next(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private extractRoleFromToken(token: string): UserRole | null {
|
||||
try {
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
const payloadJson = atob(payloadBase64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const payload = JSON.parse(payloadJson);
|
||||
return (payload.role as UserRole) || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// Registrierung mit Email und Passwort
|
||||
async registerWithEmail(email: string, password: string): Promise<UserCredential> {
|
||||
// Bestimmen der aktuellen Umgebung/Domain für die Verifizierungs-URL
|
||||
let verificationUrl = '';
|
||||
|
||||
// Prüfen der aktuellen Umgebung basierend auf dem Host
|
||||
const currentHost = window.location.hostname;
|
||||
|
||||
if (currentHost.includes('localhost')) {
|
||||
verificationUrl = 'http://localhost:4200/email-authorized';
|
||||
} else if (currentHost.includes('dev.bizmatch.net')) {
|
||||
verificationUrl = 'https://dev.bizmatch.net/email-authorized';
|
||||
} else {
|
||||
verificationUrl = 'https://www.bizmatch.net/email-authorized';
|
||||
}
|
||||
|
||||
// ActionCode-Einstellungen mit der dynamischen URL
|
||||
const actionCodeSettings = {
|
||||
url: `${verificationUrl}?email=${email}`,
|
||||
handleCodeInApp: true,
|
||||
};
|
||||
|
||||
// Benutzer erstellen
|
||||
const userCredential = await createUserWithEmailAndPassword(this.auth, email, password);
|
||||
|
||||
// E-Mail-Verifizierung mit den angepassten ActionCode-Einstellungen senden
|
||||
if (userCredential.user) {
|
||||
//await sendEmailVerification(userCredential.user, actionCodeSettings);
|
||||
this.mailService.sendVerificationEmail(userCredential.user.email).subscribe({
|
||||
next: () => {
|
||||
console.log('Verification email sent successfully');
|
||||
// Erfolgsmeldung anzeigen
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error sending verification email', error);
|
||||
// Fehlermeldung anzeigen
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// const token = await userCredential.user.getIdToken();
|
||||
// localStorage.setItem('authToken', token);
|
||||
// localStorage.setItem('refreshToken', userCredential.user.refreshToken);
|
||||
// if (userCredential.user.photoURL) {
|
||||
// localStorage.setItem('photoURL', userCredential.user.photoURL);
|
||||
// }
|
||||
|
||||
return userCredential;
|
||||
}
|
||||
|
||||
// Login mit Email und Passwort
|
||||
loginWithEmail(email: string, password: string): Promise<UserCredential> {
|
||||
return signInWithEmailAndPassword(this.auth, email, password).then(async userCredential => {
|
||||
if (userCredential.user) {
|
||||
const token = await userCredential.user.getIdToken();
|
||||
localStorage.setItem('authToken', token);
|
||||
localStorage.setItem('refreshToken', userCredential.user.refreshToken);
|
||||
if (userCredential.user.photoURL) {
|
||||
localStorage.setItem('photoURL', userCredential.user.photoURL);
|
||||
}
|
||||
this.loadRoleFromToken();
|
||||
}
|
||||
return userCredential;
|
||||
});
|
||||
}
|
||||
|
||||
// Login mit Google
|
||||
loginWithGoogle(): Promise<UserCredential> {
|
||||
const provider = new GoogleAuthProvider();
|
||||
return signInWithPopup(this.auth, provider).then(async userCredential => {
|
||||
if (userCredential.user) {
|
||||
const token = await userCredential.user.getIdToken();
|
||||
localStorage.setItem('authToken', token);
|
||||
localStorage.setItem('refreshToken', userCredential.user.refreshToken);
|
||||
if (userCredential.user.photoURL) {
|
||||
localStorage.setItem('photoURL', userCredential.user.photoURL);
|
||||
}
|
||||
this.loadRoleFromToken();
|
||||
}
|
||||
return userCredential;
|
||||
});
|
||||
}
|
||||
|
||||
// Logout: Token, RefreshToken und photoURL entfernen
|
||||
logout(): Promise<void> {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('photoURL');
|
||||
this.clearRoleCache();
|
||||
this.userRoleSubject.next(null);
|
||||
return this.auth.signOut();
|
||||
}
|
||||
isAdmin(): Observable<boolean> {
|
||||
return this.getUserRole().pipe(
|
||||
map(role => role === 'admin'),
|
||||
// take(1) ist optional - es beendet die Subscription, nachdem ein Wert geliefert wurde
|
||||
// Nützlich, wenn du die Methode in einem Template mit dem async pipe verwendest
|
||||
take(1),
|
||||
);
|
||||
}
|
||||
// Get current user's role from the server with caching
|
||||
getUserRole(): Observable<UserRole | null> {
|
||||
const now = Date.now();
|
||||
|
||||
// Cache zurücksetzen, wenn die Caching-Zeit abgelaufen ist oder kein Cache existiert
|
||||
if (!this.cachedUserRole$ || now - this.lastCacheTime > this.cacheDuration) {
|
||||
this.lastCacheTime = now;
|
||||
this.cachedUserRole$ = this.http.get<{ role: UserRole | null }>(`${environment.apiBaseUrl}/bizmatch/auth/me/role`).pipe(
|
||||
map(response => response.role),
|
||||
tap(role => this.userRoleSubject.next(role)),
|
||||
catchError(error => {
|
||||
console.error('Error fetching user role', error);
|
||||
return of(null);
|
||||
}),
|
||||
// Cache für mehrere Subscriber und behalte den letzten Wert
|
||||
// Der Parameter 1 gibt an, dass der letzte Wert gecacht werden soll
|
||||
// refCount: false bedeutet, dass der Cache nicht zurückgesetzt wird, wenn keine Subscriber mehr da sind
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
}
|
||||
|
||||
return this.cachedUserRole$;
|
||||
}
|
||||
clearRoleCache(): void {
|
||||
this.cachedUserRole$ = null;
|
||||
this.lastCacheTime = 0;
|
||||
}
|
||||
// Check if user has a specific role
|
||||
hasRole(role: UserRole): Observable<boolean> {
|
||||
return this.userRole$.pipe(
|
||||
map(userRole => {
|
||||
if (role === 'guest') {
|
||||
// Any authenticated user can access guest features
|
||||
return userRole !== null;
|
||||
} else if (role === 'pro') {
|
||||
// Both pro and admin can access pro features
|
||||
return userRole === 'pro' || userRole === 'admin';
|
||||
} else if (role === 'admin') {
|
||||
// Only admin can access admin features
|
||||
return userRole === 'admin';
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Force refresh the token to get updated custom claims
|
||||
async refreshUserClaims(): Promise<void> {
|
||||
this.clearRoleCache();
|
||||
if (this.auth.currentUser) {
|
||||
await this.auth.currentUser.getIdToken(true);
|
||||
const token = await this.auth.currentUser.getIdToken();
|
||||
localStorage.setItem('authToken', token);
|
||||
this.loadRoleFromToken();
|
||||
}
|
||||
}
|
||||
// Prüft, ob ein Token noch gültig ist (über die "exp"-Eigenschaft)
|
||||
private isTokenValid(token: string): boolean {
|
||||
try {
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
const payloadJson = atob(payloadBase64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const payload = JSON.parse(payloadJson);
|
||||
const exp = payload.exp;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return exp > now;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
private isEMailVerified(token: string): boolean {
|
||||
try {
|
||||
const payloadBase64 = token.split('.')[1];
|
||||
const payloadJson = atob(payloadBase64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const payload = JSON.parse(payloadJson);
|
||||
return payload.email_verified;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Versucht, mit dem RefreshToken einen neuen Access Token zu erhalten
|
||||
async refreshToken(): Promise<string | null> {
|
||||
const storedRefreshToken = localStorage.getItem('refreshToken');
|
||||
if (!storedRefreshToken) {
|
||||
return null;
|
||||
}
|
||||
const apiKey = environment.firebaseConfig.apiKey; // Stelle sicher, dass dieser Wert in Deiner environment.ts gesetzt ist
|
||||
const url = `https://securetoken.googleapis.com/v1/token?key=${apiKey}`;
|
||||
|
||||
const body = new HttpParams().set('grant_type', 'refresh_token').set('refresh_token', storedRefreshToken);
|
||||
|
||||
const headers = new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' });
|
||||
|
||||
try {
|
||||
const response: any = await firstValueFrom(this.http.post(url, body.toString(), { headers }));
|
||||
// response enthält z. B. id_token, refresh_token, expires_in etc.
|
||||
const newToken = response.id_token;
|
||||
const newRefreshToken = response.refresh_token;
|
||||
localStorage.setItem('authToken', newToken);
|
||||
localStorage.setItem('refreshToken', newRefreshToken);
|
||||
return newToken;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt einen gültigen Token zurück.
|
||||
* Falls der gespeicherte Token noch gültig ist, wird er zurückgegeben.
|
||||
* Ansonsten wird versucht, einen neuen Token mit dem RefreshToken zu holen.
|
||||
* Ist auch das nicht möglich, wird null zurückgegeben.
|
||||
*/
|
||||
async getToken(): Promise<string | null> {
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token && !this.isEMailVerified(token)) {
|
||||
return null;
|
||||
} else if (token && this.isTokenValid(token) && this.isEMailVerified(token)) {
|
||||
return token;
|
||||
} else {
|
||||
return await this.refreshToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ErrorHandler, Injectable } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
constructor(private keycloakService: KeycloakService) {}
|
||||
constructor(private router: Router) {}
|
||||
|
||||
handleError(error: any): void {
|
||||
// Prüfe, ob es sich um einen HttpErrorResponse handelt
|
||||
@@ -12,9 +12,7 @@ export class GlobalErrorHandler implements ErrorHandler {
|
||||
// Prüfe, ob es ein 401 Unauthorized Fehler ist
|
||||
if (error.status === 401) {
|
||||
// Führe den Login-Prozess über Keycloak aus
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href, // oder eine benutzerdefinierte URL
|
||||
});
|
||||
this.router.navigate(['/login-register']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
import { createLogger } from '../utils/utils';
|
||||
const logger = createLogger('KeycloakInitializerService');
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class KeycloakInitializerService {
|
||||
public initialized = false;
|
||||
|
||||
constructor(private keycloakService: KeycloakService) {}
|
||||
constructor() {}
|
||||
|
||||
async initialize(): Promise<boolean> {
|
||||
return new Promise<boolean>(async (resolve, reject) => {
|
||||
try {
|
||||
await this.keycloakService.init({
|
||||
config: {
|
||||
url: environment.keycloak.url,
|
||||
realm: environment.keycloak.realm,
|
||||
clientId: environment.keycloak.clientId,
|
||||
},
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
},
|
||||
bearerExcludedUrls: ['/assets'],
|
||||
});
|
||||
this.initialized = true;
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
// async initialize(): Promise<boolean> {
|
||||
// return new Promise<boolean>(async (resolve, reject) => {
|
||||
// try {
|
||||
// await this.keycloakService.init({
|
||||
// config: {
|
||||
// url: environment.keycloak.url,
|
||||
// realm: environment.keycloak.realm,
|
||||
// clientId: environment.keycloak.clientId,
|
||||
// },
|
||||
// initOptions: {
|
||||
// onLoad: 'check-sso',
|
||||
// silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
// },
|
||||
// bearerExcludedUrls: ['/assets'],
|
||||
// });
|
||||
// this.initialized = true;
|
||||
// resolve(true);
|
||||
// } catch (error) {
|
||||
// reject(error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { lastValueFrom, Observable } from 'rxjs';
|
||||
import { ShareByEMail } from '../../../../bizmatch-server/src/models/db.model';
|
||||
import { ErrorResponse, MailInfo } from '../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
@@ -18,4 +18,32 @@ export class MailService {
|
||||
async mailToFriend(shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
|
||||
return await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/mail/send2Friend`, shareByEMail));
|
||||
}
|
||||
/**
|
||||
* Sendet eine E-Mail-Verifizierung an die angegebene E-Mail-Adresse
|
||||
* @param email Die E-Mail-Adresse des Benutzers
|
||||
* @param redirectConfig Konfiguration für die Weiterleitung nach Verifizierung
|
||||
* @returns Observable mit der API-Antwort
|
||||
*/
|
||||
sendVerificationEmail(
|
||||
email: string,
|
||||
redirectConfig?: {
|
||||
protocol?: string,
|
||||
hostname?: string,
|
||||
port?: number
|
||||
}
|
||||
): Observable<any> {
|
||||
// Extrahiere aktuelle URL-Informationen, wenn nicht explizit angegeben
|
||||
const currentUrl = new URL(window.location.href);
|
||||
|
||||
const config = {
|
||||
protocol: redirectConfig?.protocol || currentUrl.protocol.replace(':', ''),
|
||||
hostname: redirectConfig?.hostname || currentUrl.hostname,
|
||||
port: redirectConfig?.port || (currentUrl.port ? parseInt(currentUrl.port) : undefined)
|
||||
};
|
||||
|
||||
return this.http.post(`${this.apiBaseUrl}/bizmatch/mail/verify-email`, {
|
||||
email,
|
||||
redirectConfig: config
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -181,7 +181,7 @@ export function routeListingWithState(router: Router, value: string, data: any)
|
||||
}
|
||||
}
|
||||
|
||||
export function map2User(jwt: string): KeycloakUser {
|
||||
export function map2User(jwt: string | null): KeycloakUser {
|
||||
if (jwt) {
|
||||
const token = jwtDecode<JwtToken>(jwt);
|
||||
return {
|
||||
@@ -340,3 +340,6 @@ export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPro
|
||||
}
|
||||
});
|
||||
}
|
||||
// export function isAdmin(email: string) {
|
||||
// return 'andreas.knuth@gmail.com' === email;
|
||||
// }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user