Compare commits
20 Commits
b9a9b983e9
...
bizmatch
| Author | SHA1 | Date | |
|---|---|---|---|
| 9235cb0f22 | |||
| eb23bebc10 | |||
| b39370a6b5 | |||
| 6b97008643 | |||
| 715fbdf2f5 | |||
| 923040f487 | |||
| cfddabbfe0 | |||
| 097a6cb360 | |||
| 162c5b042f | |||
| 9e8f67d647 | |||
| 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,139 @@
|
||||
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 {
|
||||
// Step 1: Get the user by email address
|
||||
const userRecord = await this.firebaseAdmin.auth().getUserByEmail(email);
|
||||
|
||||
if (userRecord.emailVerified) {
|
||||
// Even if already verified, we'll still return a valid token
|
||||
const customToken = await this.firebaseAdmin.auth().createCustomToken(userRecord.uid);
|
||||
return {
|
||||
message: 'Email is already verified',
|
||||
token: customToken,
|
||||
};
|
||||
}
|
||||
|
||||
// Step 2: Update the user status to set emailVerified to true
|
||||
await this.firebaseAdmin.auth().updateUser(userRecord.uid, {
|
||||
emailVerified: true,
|
||||
});
|
||||
|
||||
// Step 3: Generate a custom Firebase token for the user
|
||||
// This token can be used on the client side to authenticate with Firebase
|
||||
const customToken = await this.firebaseAdmin.auth().createCustomToken(userRecord.uid);
|
||||
|
||||
return {
|
||||
message: 'Email successfully verified',
|
||||
token: customToken,
|
||||
};
|
||||
} 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'),
|
||||
@@ -72,14 +73,6 @@ export const businesses = pgTable(
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
location: jsonb('location'),
|
||||
// city: varchar('city', { length: 255 }),
|
||||
// state: char('state', { length: 2 }),
|
||||
// zipCode: integer('zipCode'),
|
||||
// county: varchar('county', { length: 255 }),
|
||||
// street: varchar('street', { length: 255 }),
|
||||
// housenumber: varchar('housenumber', { length: 10 }),
|
||||
// latitude: doublePrecision('latitude'),
|
||||
// longitude: doublePrecision('longitude'),
|
||||
},
|
||||
table => ({
|
||||
locationBusinessCityStateIdx: index('idx_business_location_city_state').on(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { and, arrayContains, asc, count, desc, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
@@ -103,8 +103,8 @@ export class BusinessListingService {
|
||||
whereConditions.push(and(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
|
||||
}
|
||||
}
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
whereConditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true)));
|
||||
if (user?.role !== 'admin') {
|
||||
whereConditions.push(or(eq(businesses.email, user?.email), ne(businesses.draft, true)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
|
||||
return whereConditions;
|
||||
@@ -186,8 +186,8 @@ export class BusinessListingService {
|
||||
|
||||
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
|
||||
const conditions = [];
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
conditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true)));
|
||||
if (user?.role !== 'admin') {
|
||||
conditions.push(or(eq(businesses.email, user?.email), ne(businesses.draft, true)));
|
||||
}
|
||||
conditions.push(sql`${businesses.id} = ${id}`);
|
||||
const result = await this.conn
|
||||
@@ -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?.email && user?.role !== 'admin') {
|
||||
conditions.push(ne(businesses.draft, true));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
@@ -219,7 +219,7 @@ export class BusinessListingService {
|
||||
const userFavorites = await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.where(arrayContains(businesses.favoritesForUser, [user.username]));
|
||||
.where(arrayContains(businesses.favoritesForUser, [user.email]));
|
||||
return userFavorites;
|
||||
}
|
||||
// #### CREATE ########################################
|
||||
@@ -246,10 +246,18 @@ export class BusinessListingService {
|
||||
}
|
||||
}
|
||||
// #### UPDATE Business ########################################
|
||||
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> {
|
||||
async updateBusinessListing(id: string, data: BusinessListing, user: JwtUser): Promise<BusinessListing> {
|
||||
try {
|
||||
const [existingListing] = await this.conn.select().from(businesses).where(eq(businesses.id, id));
|
||||
|
||||
if (!existingListing) {
|
||||
throw new NotFoundException(`Business listing with id ${id} not found`);
|
||||
}
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
if (existingListing.email === user?.email) {
|
||||
data.favoritesForUser = existingListing.favoritesForUser;
|
||||
}
|
||||
BusinessListingSchema.parse(data);
|
||||
const convertedBusinessListing = data;
|
||||
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
|
||||
@@ -276,7 +284,7 @@ export class BusinessListingService {
|
||||
await this.conn
|
||||
.update(businesses)
|
||||
.set({
|
||||
favoritesForUser: sql`array_remove(${businesses.favoritesForUser}, ${user.username})`,
|
||||
favoritesForUser: sql`array_remove(${businesses.favoritesForUser}, ${user.email})`,
|
||||
})
|
||||
.where(sql`${businesses.id} = ${id}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
async update(@Request() req, @Body() listing: any) {
|
||||
return await this.listingsService.updateBusinessListing(listing.id, listing, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@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);
|
||||
async update(@Request() req, @Body() listing: any) {
|
||||
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { and, arrayContains, asc, count, desc, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
@@ -49,8 +49,8 @@ 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) {
|
||||
whereConditions.push(or(eq(commercials.email, user?.username), ne(commercials.draft, true)));
|
||||
if (user?.role !== 'admin') {
|
||||
whereConditions.push(or(eq(commercials.email, user?.email), ne(commercials.draft, true)));
|
||||
}
|
||||
// whereConditions.push(and(eq(schema.users.customerType, 'professional')));
|
||||
return whereConditions;
|
||||
@@ -113,8 +113,8 @@ export class CommercialPropertyService {
|
||||
// #### Find by ID ########################################
|
||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
const conditions = [];
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
conditions.push(or(eq(commercials.email, user?.username), ne(commercials.draft, true)));
|
||||
if (user?.role !== 'admin') {
|
||||
conditions.push(or(eq(commercials.email, user?.email), ne(commercials.draft, true)));
|
||||
}
|
||||
conditions.push(sql`${commercials.id} = ${id}`);
|
||||
const result = await this.conn
|
||||
@@ -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?.email && user?.role !== 'admin') {
|
||||
conditions.push(ne(commercials.draft, true));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
@@ -146,7 +146,7 @@ export class CommercialPropertyService {
|
||||
const userFavorites = await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(arrayContains(commercials.favoritesForUser, [user.username]));
|
||||
.where(arrayContains(commercials.favoritesForUser, [user.email]));
|
||||
return userFavorites;
|
||||
}
|
||||
// #### Find by imagePath ########################################
|
||||
@@ -181,10 +181,18 @@ export class CommercialPropertyService {
|
||||
}
|
||||
}
|
||||
// #### UPDATE CommercialProps ########################################
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
try {
|
||||
const [existingListing] = await this.conn.select().from(commercials).where(eq(commercials.id, id));
|
||||
|
||||
if (!existingListing) {
|
||||
throw new NotFoundException(`Business listing with id ${id} not found`);
|
||||
}
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
if (existingListing.email === user?.email || !user) {
|
||||
data.favoritesForUser = existingListing.favoritesForUser;
|
||||
}
|
||||
CommercialPropertyListingSchema.parse(data);
|
||||
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
|
||||
const difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
|
||||
@@ -216,13 +224,13 @@ export class CommercialPropertyService {
|
||||
const index = listing.imageOrder.findIndex(im => im === name);
|
||||
if (index > -1) {
|
||||
listing.imageOrder.splice(index, 1);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing, null);
|
||||
}
|
||||
}
|
||||
async addImage(imagePath: string, serial: string, imagename: string) {
|
||||
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||
listing.imageOrder.push(imagename);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing, null);
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
@@ -233,7 +241,7 @@ export class CommercialPropertyService {
|
||||
await this.conn
|
||||
.update(commercials)
|
||||
.set({
|
||||
favoritesForUser: sql`array_remove(${commercials.favoritesForUser}, ${user.username})`,
|
||||
favoritesForUser: sql`array_remove(${commercials.favoritesForUser}, ${user.email})`,
|
||||
})
|
||||
.where(sql`${commercials.id} = ${id}`);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -165,8 +165,8 @@ const phoneRegex = /^(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
|
||||
export const UserSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
firstname: z.string().min(2, { message: 'First name must contain at least 2 characters' }),
|
||||
lastname: z.string().min(2, { message: 'Last name must contain at least 2 characters' }),
|
||||
firstname: z.string().min(3, { message: 'First name must contain at least 2 characters' }),
|
||||
lastname: z.string().min(3, { message: 'Last name must contain at least 2 characters' }),
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
phoneNumber: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
@@ -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') {
|
||||
@@ -196,7 +197,13 @@ export const UserSchema = z
|
||||
path: ['customerSubType'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.companyName || data.companyName.length < 6) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company Name must contain at least 6 characters for professional customers',
|
||||
path: ['companyName'],
|
||||
});
|
||||
}
|
||||
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@@ -233,7 +240,7 @@ export const UserSchema = z
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company location is required for professional customers',
|
||||
path: ['companyLocation'],
|
||||
path: ['location'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -251,7 +258,8 @@ export type AreasServed = z.infer<typeof AreasServedSchema>;
|
||||
export type LicensedIn = z.infer<typeof LicensedInSchema>;
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
export const BusinessListingSchema = z.object({
|
||||
export const BusinessListingSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
email: z.string().email(),
|
||||
type: z.string().refine(val => TypeEnum.safeParse(val).success, {
|
||||
@@ -260,14 +268,14 @@ export const BusinessListingSchema = z.object({
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
price: z.number().positive(),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
draft: z.boolean(),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
realEstateIncluded: z.boolean().optional().nullable(),
|
||||
leasedLocation: z.boolean().optional().nullable(),
|
||||
franchiseResale: z.boolean().optional().nullable(),
|
||||
salesRevenue: z.number().positive().max(100000000),
|
||||
salesRevenue: z.number().positive().nullable(),
|
||||
cashFlow: z.number().positive().max(100000000),
|
||||
supportAndTraining: z.string().min(5),
|
||||
employees: z.number().int().positive().max(100000).optional().nullable(),
|
||||
@@ -279,7 +287,30 @@ export const BusinessListingSchema = z.object({
|
||||
imageName: z.string().optional().nullable(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
});
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.price && data.price > 1000000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Price must less than or equal $1,000,000,000',
|
||||
path: ['price'],
|
||||
});
|
||||
}
|
||||
if (data.salesRevenue && data.salesRevenue > 100000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'SalesRevenue must less than or equal $100,000,000',
|
||||
path: ['salesRevenue'],
|
||||
});
|
||||
}
|
||||
if (data.cashFlow && data.cashFlow > 100000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'CashFlow must less than or equal $100,000,000',
|
||||
path: ['cashFlow'],
|
||||
});
|
||||
}
|
||||
});
|
||||
export type BusinessListing = z.infer<typeof BusinessListingSchema>;
|
||||
|
||||
export const CommercialPropertyListingSchema = z
|
||||
@@ -293,7 +324,7 @@ export const CommercialPropertyListingSchema = z
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
price: z.number().positive(),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
draft: z.boolean(),
|
||||
@@ -302,7 +333,15 @@ export const CommercialPropertyListingSchema = z
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
})
|
||||
.strict();
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.price && data.price > 1000000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Price must less than or equal $1,000,000,000',
|
||||
path: ['price'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CommercialPropertyListing = z.infer<typeof CommercialPropertyListingSchema>;
|
||||
|
||||
|
||||
@@ -123,11 +123,9 @@ export interface KeycloakUser {
|
||||
attributes?: Attributes;
|
||||
}
|
||||
export interface JwtUser {
|
||||
userId: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
roles: string[];
|
||||
email: string;
|
||||
role: string;
|
||||
uid: string;
|
||||
}
|
||||
interface Attributes {
|
||||
[key: string]: any;
|
||||
@@ -278,6 +276,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, '', '', 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",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --host 0.0.0.0 & http-server ../bizmatch-server",
|
||||
"start": "ng serve --port=4300 --host 0.0.0.0 & http-server ../bizmatch-server",
|
||||
"prebuild": "node version.js",
|
||||
"build": "node version.js && ng build",
|
||||
"build.dev": "node version.js && ng build --configuration dev --output-hashing=all",
|
||||
@@ -18,18 +18,19 @@
|
||||
"@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/google-maps": "^18.2.14",
|
||||
"@angular/platform-browser": "^18.1.3",
|
||||
"@angular/platform-browser-dynamic": "^18.1.3",
|
||||
"@angular/platform-server": "^18.1.3",
|
||||
"@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 +42,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,12 +1,11 @@
|
||||
<!-- <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">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -45,40 +43,11 @@ export class AppComponent {
|
||||
while (currentRoute.children[0] !== undefined) {
|
||||
currentRoute = currentRoute.children[0];
|
||||
}
|
||||
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad
|
||||
this.actualRoute = currentRoute.snapshot.url[0].path;
|
||||
});
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ngOnInit() {}
|
||||
|
||||
@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';
|
||||
@@ -26,17 +28,14 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'businessListings',
|
||||
component: BusinessListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'commercialPropertyListings',
|
||||
component: CommercialPropertyListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'brokerListings',
|
||||
component: BrokerListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
@@ -57,9 +56,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 +146,14 @@ export const routes: Routes = [
|
||||
path: 'pricing',
|
||||
component: PricingComponent,
|
||||
},
|
||||
{
|
||||
path: 'emailVerification',
|
||||
component: EmailVerificationComponent,
|
||||
},
|
||||
{
|
||||
path: 'email-authorized',
|
||||
component: EmailAuthorizedComponent,
|
||||
},
|
||||
{
|
||||
path: 'pricingOverview',
|
||||
component: PricingComponent,
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
<div #_container class="container">
|
||||
<!-- <div
|
||||
*ngFor="let item of items"
|
||||
cdkDrag
|
||||
(cdkDragEnded)="dragEnded($event)"
|
||||
(cdkDragStarted)="dragStarted()"
|
||||
(cdkDragMoved)="dragMoved($event)"
|
||||
class="item"
|
||||
[class.animation]="isAnimationActive"
|
||||
[class.large]="item === 3"
|
||||
>
|
||||
Drag Item {{ item }}
|
||||
</div> -->
|
||||
<div *ngFor="let item of items" cdkDrag (cdkDragEnded)="dragEnded($event)" (cdkDragStarted)="dragStarted()" (cdkDragMoved)="dragMoved($event)" [class.animation]="isAnimationActive" class="grid-item item">
|
||||
<div class="image-box hover:cursor-pointer">
|
||||
<img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg shadow-md" />
|
||||
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md" (click)="imageToDelete.emit(item)">
|
||||
<img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg drop-shadow-custom-bg" />
|
||||
<div class="absolute top-2 right-2 bg-white rounded-full p-1 drop-shadow-custom-bg" (click)="imageToDelete.emit(item)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="container mx-auto py-8 px-4 max-w-md">
|
||||
<div class="bg-white p-6 rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg text-center">
|
||||
<!-- Loading state -->
|
||||
<ng-container *ngIf="verificationStatus === 'pending'">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<p class="text-gray-700">Verifying your email address...</p>
|
||||
</ng-container>
|
||||
|
||||
<!-- Success state -->
|
||||
<ng-container *ngIf="verificationStatus === 'success'">
|
||||
<div class="flex justify-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<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 will be redirected to your account page in 5 seconds</p>
|
||||
<a routerLink="/account" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Go to Account Page Now </a>
|
||||
</ng-container>
|
||||
|
||||
<!-- Error state -->
|
||||
<ng-container *ngIf="verificationStatus === 'error'">
|
||||
<div class="flex justify-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-red-500" 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>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-red-600 mb-3">Verification Failed</h2>
|
||||
<p class="text-gray-700 mb-4">{{ errorMessage }}</p>
|
||||
<a routerLink="/login" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Return to Login </a>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router, 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 router: Router, 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<{ message: string; token: string }>(`${environment.apiBaseUrl}/bizmatch/auth/verify-email`, { oobCode, email }).subscribe({
|
||||
next: async response => {
|
||||
this.verificationStatus = 'success';
|
||||
|
||||
try {
|
||||
// Use the custom token from the server to sign in with Firebase
|
||||
await this.authService.signInWithCustomToken(response.token);
|
||||
|
||||
// Try to get user info
|
||||
try {
|
||||
const user = await this.userService.getByMail(email);
|
||||
console.log('User retrieved:', user);
|
||||
} catch (userError) {
|
||||
console.error('Error getting user:', userError);
|
||||
// Don't change verification status - it's still a success
|
||||
}
|
||||
|
||||
// Redirect to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/account']);
|
||||
}, 5000);
|
||||
} catch (authError) {
|
||||
console.error('Error signing in with custom token:', authError);
|
||||
// Keep success status for verification, but add warning about login
|
||||
this.errorMessage = 'Email verified, but there was an issue signing you in. Please try logging in manually.';
|
||||
}
|
||||
},
|
||||
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 drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg 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 {}
|
||||
@@ -1,4 +1,5 @@
|
||||
<footer class="bg-white px-4 py-2 md:px-6 mt-auto w-full print:hidden">
|
||||
<ng-template #otherRoute>
|
||||
<footer class="bg-white px-4 py-2 md:px-6 mt-auto w-full print:hidden">
|
||||
<div class="container mx-auto flex flex-col lg:flex-row justify-between items-center">
|
||||
<div class="flex flex-col lg:flex-row items-center mb-4 lg:mb-0">
|
||||
<!-- <img src="assets/images/header-logo.png" alt="BizMatch Logo" class="h-8 mb-2 lg:mb-0 lg:mr-4" /> -->
|
||||
@@ -11,7 +12,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">
|
||||
@@ -26,6 +27,55 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</ng-template>
|
||||
<footer *ngIf="isHomeRoute; else otherRoute" class="bg-gray-800 text-white pt-12 pb-4">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-full md:w-1/3 mb-8 md:mb-0">
|
||||
<h3 class="text-xl font-semibold mb-4">BizMatch</h3>
|
||||
<p class="mb-2">Your trusted partner in business brokerage.</p>
|
||||
<p class="mb-2">TREC License #0516 788</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-1/3 mb-8 md:mb-0">
|
||||
<h3 class="text-xl font-semibold mb-4">Quick Links</h3>
|
||||
<ul>
|
||||
<li class="mb-2">
|
||||
<a href="#" class="text-gray-300 hover:text-white">Home</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a href="#services" class="text-gray-300 hover:text-white">Services</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a href="#location" class="text-gray-300 hover:text-white">Location</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a href="#contact" class="text-gray-300 hover:text-white">Contact</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a data-drawer-target="terms-of-use" data-drawer-show="terms-of-use" aria-controls="terms-of-use" class="text-gray-300 hover:text-white">Terms of use</a>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<a data-drawer-target="privacy" data-drawer-show="privacy" aria-controls="privacy" class="text-gray-300 hover:text-white">Privacy statement</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-1/3">
|
||||
<h3 class="text-xl font-semibold mb-4">Contact Us</h3>
|
||||
<p class="mb-2">1001 Blucher Street</p>
|
||||
<p class="mb-2">Corpus Christi, TX 78401</p>
|
||||
<p class="mb-4">United States</p>
|
||||
<p class="mb-2">1-800-840-6025</p>
|
||||
<p class="mb-2">info@bizmatch.net</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 text-center">
|
||||
<p class="text-sm text-gray-400 mt-4">© 2025 BizMatch. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div id="privacy" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white lg:w-1/3 w-96 dark:bg-gray-800" tabindex="-1" aria-labelledby="drawer-label">
|
||||
<h5 id="drawer-label" class="inline-flex items-center mb-4 text-base font-semibold text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
:host {
|
||||
// position: absolute;
|
||||
// bottom: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
div {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.order-2 {
|
||||
order: 2;
|
||||
|
||||
@@ -15,10 +15,12 @@ export class FooterComponent {
|
||||
privacyVisible = false;
|
||||
termsVisible = false;
|
||||
currentYear: number = new Date().getFullYear();
|
||||
isHomeRoute = false;
|
||||
constructor(private router: Router) {}
|
||||
ngOnInit() {
|
||||
this.router.events.subscribe(event => {
|
||||
if (event instanceof NavigationEnd) {
|
||||
this.isHomeRoute = event.url === '/home';
|
||||
initFlowbite();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<nav class="bg-white border-gray-200 dark:bg-gray-900 print:hidden">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<div class="flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
|
||||
</a>
|
||||
@@ -28,16 +28,11 @@
|
||||
</button>
|
||||
|
||||
<!-- Sort options dropdown -->
|
||||
<div *ngIf="sortDropdownVisible" class="absolute right-0 z-50 w-48 md:mt-2 max-md:mt-20 max-md:mr-[-2.5rem] bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-600">
|
||||
<div *ngIf="sortDropdownVisible" class="absolute right-0 z-50 w-48 md:mt-2 max-md:mt-20 max-md:mr-[-2.5rem] bg-white border border-gray-200 rounded-lg drop-shadow-custom-bg dark:bg-gray-800 dark:border-gray-600">
|
||||
<ul class="py-1 text-sm text-gray-700 dark:text-gray-200">
|
||||
@for(item of sortByOptions; track item){
|
||||
<li (click)="sortBy(item.value)" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">{{ item.selectName ? item.selectName : item.name }}</li>
|
||||
}
|
||||
<!-- <li (click)="sortBy('priceAsc')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Price Ascending</li>
|
||||
<li (click)="sortBy('priceDesc')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Price Descending</li>
|
||||
<li (click)="sortBy('creationDateFirst')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Creation Date First</li>
|
||||
<li (click)="sortBy('creationDateLast')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Creation Date Last</li>
|
||||
<li (click)="sortBy(null)" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Default Sorting</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +46,7 @@
|
||||
data-dropdown-placement="bottom"
|
||||
>
|
||||
<span class="sr-only">Open user menu</span>
|
||||
@if(user?.hasProfile){
|
||||
@if(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
|
||||
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="user photo" />
|
||||
} @else {
|
||||
<i class="flex justify-center items-center text-stone-50 w-8 h-8 rounded-full fa-solid fa-bars"></i>
|
||||
@@ -68,40 +63,31 @@
|
||||
<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(isProfessional || (authService.isAdmin() | async) && user?.hasProfile){
|
||||
<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"
|
||||
>Create Listing</a
|
||||
>
|
||||
}@else {
|
||||
<a routerLink="/createCommercialPropertyListing" (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"
|
||||
>Create Listing</a
|
||||
>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a routerLink="/myListings" (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">My Listings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/myFavorites" (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">My Favorites</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a routerLink="/emailUs" (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">EMail Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
<ul class="py-2 md:hidden">
|
||||
<!-- <ul class="py-2 md:hidden">
|
||||
<li>
|
||||
<a
|
||||
routerLink="/businessListings"
|
||||
@@ -111,6 +97,7 @@
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
@@ -120,6 +107,7 @@
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
@@ -129,19 +117,20 @@
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
</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>
|
||||
</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: '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="/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">Sign Up</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
<ul class="py-2 md:hidden">
|
||||
<!-- <ul class="py-2 md:hidden">
|
||||
<li>
|
||||
<a
|
||||
routerLink="/businessListings"
|
||||
@@ -151,6 +140,7 @@
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
@@ -160,6 +150,7 @@
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
@@ -169,23 +160,12 @@
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
</ul> -->
|
||||
</div>
|
||||
}
|
||||
<!-- <button
|
||||
data-collapse-toggle="navbar-user"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
aria-controls="navbar-user"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
|
||||
</svg>
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||
<!-- <div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||
<ul
|
||||
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"
|
||||
>
|
||||
@@ -200,6 +180,7 @@
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
@if ((numberOfCommercial$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
@@ -210,6 +191,7 @@
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
} @if ((numberOfBroker$ | async) > 0) {
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
@@ -220,11 +202,11 @@
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- Mobile filter button -->
|
||||
@if(isFilterUrl()){
|
||||
<div class="md:hidden flex justify-center pb-4">
|
||||
<button
|
||||
(click)="openModal()"
|
||||
@@ -245,5 +227,4 @@
|
||||
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(criteria?.sortBy) }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
|
||||
@@ -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 { debounceTime, 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);
|
||||
@@ -87,7 +92,9 @@ export class HeaderComponent {
|
||||
this.checkCurrentRoute(event.urlAfterRedirects);
|
||||
this.setupSortByOptions();
|
||||
});
|
||||
|
||||
this.subscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
|
||||
this.criteria = getCriteriaProxy(this.baseRoute, this);
|
||||
});
|
||||
this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => {
|
||||
this.user = u;
|
||||
});
|
||||
@@ -124,15 +131,13 @@ 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;
|
||||
}
|
||||
isEmailUsUrl(): boolean {
|
||||
return ['/emailUs'].includes(this.router.url);
|
||||
}
|
||||
isFilterUrl(): boolean {
|
||||
return ['/businessListings', '/commercialPropertyListings', '/brokerListings'].includes(this.router.url);
|
||||
}
|
||||
@@ -189,9 +194,7 @@ export class HeaderComponent {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
isAdmin() {
|
||||
return this.keycloakService.getUserRoles(true).includes('ADMIN');
|
||||
}
|
||||
|
||||
sortBy(sortBy: SortByOptions) {
|
||||
this.criteria.sortBy = sortBy;
|
||||
this.sortDropdownVisible = false;
|
||||
@@ -200,4 +203,7 @@ export class HeaderComponent {
|
||||
toggleSortDropdown() {
|
||||
this.sortDropdownVisible = !this.sortDropdownVisible;
|
||||
}
|
||||
get isProfessional() {
|
||||
return this.user?.customerType === 'professional';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<div class="flex flex-col items-center justify-center min-h-screen">
|
||||
<div class="bg-white p-8 rounded-lg drop-shadow-custom-bg 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="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,97 @@
|
||||
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.clearRoleCache();
|
||||
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.clearRoleCache();
|
||||
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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
[items]="counties$ | async"
|
||||
bindLabel="name"
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
[multiple]="true"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
|
||||
@@ -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 drop-shadow-custom-bg 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 drop-shadow-custom-bg 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}`;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden relative">
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden relative">
|
||||
<button
|
||||
(click)="historyService.goBack()"
|
||||
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 print:hidden"
|
||||
@@ -14,17 +14,21 @@
|
||||
<p class="mb-4" [innerHTML]="description"></p>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-gray-100': i % 2 === 0 }" *ngFor="let item of listingDetails; let i = index">
|
||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ item.label }}</div>
|
||||
@if(item.label==='Category'){
|
||||
<span class="bg-blue-100 text-blue-800 font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-blue-900 dark:text-blue-300 my-1">{{ item.value }}</span>
|
||||
} @else {
|
||||
<div class="w-full sm:w-2/3 p-2">{{ item.value }}</div>
|
||||
}
|
||||
<div *ngFor="let detail of listingDetails; let i = index" class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-gray-100': i % 2 === 0 }">
|
||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
||||
|
||||
<div class="w-full sm:w-2/3 p-2" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value }}</div>
|
||||
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value" *ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy">
|
||||
<a routerLink="/details-user/{{ listingUser.id }}" class="text-blue-600 dark:text-blue-500 hover:underline">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
|
||||
</div>
|
||||
</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>
|
||||
@@ -84,17 +88,6 @@
|
||||
<div>
|
||||
<app-validated-textarea label="Questions/Comments" name="comments" [(ngModel)]="mailinfo.sender.comments"></app-validated-textarea>
|
||||
</div>
|
||||
@if(listingUser){
|
||||
<div class="flex items-center space-x-2">
|
||||
<p>Listing by</p>
|
||||
<!-- <p class="text-sm font-semibold">Noah Nguyen</p> -->
|
||||
<a routerLink="/details-user/{{ listingUser.id }}" class="text-blue-600 dark:text-blue-500 hover:underline">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
||||
<!-- <img src="https://placehold.co/20x20" alt="Broker logo" class="w-5 h-5" /> -->
|
||||
@if(listingUser.hasCompanyLogo){
|
||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button (click)="mail()" class="w-full sm:w-auto px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ChangeDetectorRef, 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,8 @@ import { SharedModule } from '../../../shared/shared/shared.module';
|
||||
import { createMailInfo, map2User } from '../../../utils/utils';
|
||||
// Import für Leaflet
|
||||
// Benannte Importe für Leaflet
|
||||
import dayjs from 'dayjs';
|
||||
import { AuthService } from '../../../services/auth.service';
|
||||
import { BaseDetailsComponent } from '../base-details.component';
|
||||
@Component({
|
||||
selector: 'app-details-business-listing',
|
||||
@@ -74,12 +75,13 @@ 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,
|
||||
private cdref: ChangeDetectorRef,
|
||||
) {
|
||||
super();
|
||||
this.router.events.subscribe(event => {
|
||||
@@ -92,7 +94,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,16 +117,16 @@ 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;
|
||||
this.mailinfo.listing = this.listing;
|
||||
await this.mailService.mail(this.mailinfo);
|
||||
this.validationMessagesService.clearMessages();
|
||||
this.auditService.createEvent(this.listing.id, 'contact', this.user?.email, this.mailinfo.sender);
|
||||
this.messageService.addMessage({ severity: 'success', text: 'Your message has been sent to the creator of the listing', duration: 3000 });
|
||||
this.mailinfo = createMailInfo(this.user);
|
||||
} catch (error) {
|
||||
this.messageService.addMessage({
|
||||
severity: 'danger',
|
||||
@@ -135,9 +137,6 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
this.validationMessagesService.updateMessages(error.error.message);
|
||||
}
|
||||
}
|
||||
if (this.user) {
|
||||
this.mailinfo = createMailInfo(this.user);
|
||||
}
|
||||
}
|
||||
get listingDetails() {
|
||||
let typeOfRealEstate = '';
|
||||
@@ -151,15 +150,26 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
const result = [
|
||||
{ label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) },
|
||||
{ label: 'Located in', value: `${this.listing.location.name ? this.listing.location.name : this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}` },
|
||||
{ label: 'Asking Price', value: `$${this.listing.price?.toLocaleString()}` },
|
||||
{ label: 'Sales revenue', value: `$${this.listing.salesRevenue?.toLocaleString()}` },
|
||||
{ label: 'Cash flow', value: `$${this.listing.cashFlow?.toLocaleString()}` },
|
||||
{ label: 'Asking Price', value: `${this.listing.price ? `$${this.listing.price.toLocaleString()}` : ''}` },
|
||||
{ label: 'Sales revenue', value: `${this.listing.salesRevenue ? `$${this.listing.salesRevenue.toLocaleString()}` : ''}` },
|
||||
{ label: 'Cash flow', value: `${this.listing.cashFlow ? `$${this.listing.cashFlow.toLocaleString()}` : ''}` },
|
||||
{ label: 'Type of Real Estate', value: typeOfRealEstate },
|
||||
{ label: 'Employees', value: this.listing.employees },
|
||||
{ label: 'Established since', value: this.listing.established },
|
||||
{ label: 'Support & Training', value: this.listing.supportAndTraining },
|
||||
{ label: 'Reason for Sale', value: this.listing.reasonForSale },
|
||||
{ label: 'Broker licensing', value: this.listing.brokerLicencing },
|
||||
{ label: 'Listed since', value: `${this.dateInserted()} - ${this.getDaysListed()} days` },
|
||||
{
|
||||
label: 'Listing by',
|
||||
value: null, // Wird nicht verwendet
|
||||
isHtml: true,
|
||||
isListingBy: true, // Flag für den speziellen Fall
|
||||
user: this.listingUser, // Übergebe das User-Objekt
|
||||
imagePath: this.listing.imageName,
|
||||
imageBaseUrl: this.env.imageBaseUrl,
|
||||
ts: this.ts,
|
||||
},
|
||||
];
|
||||
if (this.listing.draft) {
|
||||
result.push({ label: 'Draft', value: this.listing.draft ? 'Yes' : 'No' });
|
||||
@@ -196,4 +206,10 @@ export class DetailsBusinessListingComponent extends BaseDetailsComponent {
|
||||
createEvent(eventType: EventTypeEnum) {
|
||||
this.auditService.createEvent(this.listing.id, eventType, this.user?.email);
|
||||
}
|
||||
getDaysListed() {
|
||||
return dayjs().diff(this.listing.created, 'day');
|
||||
}
|
||||
dateInserted() {
|
||||
return dayjs(this.listing.created).format('DD/MM/YYYY');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<div class="bg-white drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg rounded-lg overflow-hidden">
|
||||
@if(listing){
|
||||
<div class="p-6 relative">
|
||||
<h1 class="text-3xl font-bold mb-4">{{ listing?.title }}</h1>
|
||||
@@ -16,11 +16,22 @@
|
||||
<div class="space-y-2">
|
||||
<div *ngFor="let detail of propertyDetails; let i = index" class="flex flex-col sm:flex-row" [ngClass]="{ 'bg-gray-100': i % 2 === 0 }">
|
||||
<div class="w-full sm:w-1/3 font-semibold p-2">{{ detail.label }}</div>
|
||||
<div class="w-full sm:w-2/3 p-2">{{ detail.value }}</div>
|
||||
|
||||
<!-- Standard Text -->
|
||||
<div class="w-full sm:w-2/3 p-2" *ngIf="!detail.isHtml && !detail.isListingBy">{{ detail.value }}</div>
|
||||
|
||||
<!-- HTML Content (nicht für RouterLink) -->
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" [innerHTML]="detail.value" *ngIf="detail.isHtml && !detail.isListingBy"></div>
|
||||
|
||||
<!-- Speziell für Listing By mit RouterLink -->
|
||||
<div class="w-full sm:w-2/3 p-2 flex space-x-2" *ngIf="detail.isListingBy">
|
||||
<a [routerLink]="['/details-user', detail.user.id]" class="text-blue-600 dark:text-blue-500 hover:underline"> {{ detail.user.firstname }} {{ detail.user.lastname }} </a>
|
||||
<img *ngIf="detail.user.hasCompanyLogo" [src]="detail.imageBaseUrl + '/pictures/logo/' + detail.imagePath + '.avif?_ts=' + detail.ts" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
|
||||
</div>
|
||||
</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>
|
||||
@@ -88,15 +99,6 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
@if(listingUser){
|
||||
<div class="flex items-center space-x-2">
|
||||
<p>Listing by</p>
|
||||
<a routerLink="/details-user/{{ listingUser.id }}" class="text-blue-600 dark:text-blue-500 hover:underline">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
||||
@if(listingUser.hasCompanyLogo){
|
||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imagePath }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button (click)="mail()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 dayjs from 'dayjs';
|
||||
import { GalleryModule, ImageItem } from 'ng-gallery';
|
||||
import { ShareButton } from 'ngx-sharebuttons/button';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
@@ -17,6 +17,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 +78,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);
|
||||
@@ -109,6 +110,17 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
{ label: 'Located in', value: this.selectOptions.getState(this.listing.location.state) },
|
||||
{ label: this.listing.location.name ? 'City' : 'County', value: this.listing.location.name ? this.listing.location.name : this.listing.location.county },
|
||||
{ label: 'Asking Price:', value: `$${this.listing.price?.toLocaleString()}` },
|
||||
{ label: 'Listed since', value: `${this.dateInserted()} - ${this.getDaysListed()} days` },
|
||||
{
|
||||
label: 'Listing by',
|
||||
value: null, // Wird nicht verwendet
|
||||
isHtml: true,
|
||||
isListingBy: true, // Flag für den speziellen Fall
|
||||
user: this.listingUser, // Übergebe das User-Objekt
|
||||
imagePath: this.listing.imagePath,
|
||||
imageBaseUrl: this.env.imageBaseUrl,
|
||||
ts: this.ts,
|
||||
},
|
||||
];
|
||||
if (this.listing.draft) {
|
||||
this.propertyDetails.push({ label: 'Draft', value: this.listing.draft ? 'Yes' : 'No' });
|
||||
@@ -139,16 +151,15 @@ 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;
|
||||
this.mailinfo.listing = this.listing;
|
||||
await this.mailService.mail(this.mailinfo);
|
||||
this.validationMessagesService.clearMessages();
|
||||
this.auditService.createEvent(this.listing.id, 'contact', this.user?.email, this.mailinfo.sender);
|
||||
this.messageService.addMessage({ severity: 'success', text: 'Your message has been sent to the creator of the listing', duration: 3000 });
|
||||
this.mailinfo = createMailInfo(this.user);
|
||||
} catch (error) {
|
||||
this.messageService.addMessage({
|
||||
severity: 'danger',
|
||||
@@ -159,9 +170,6 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
this.validationMessagesService.updateMessages(error.error.message);
|
||||
}
|
||||
}
|
||||
if (this.user) {
|
||||
this.mailinfo = createMailInfo(this.user);
|
||||
}
|
||||
}
|
||||
containsError(fieldname: string) {
|
||||
return this.errorResponse?.fields.map(f => f.fieldname).includes(fieldname);
|
||||
@@ -198,4 +206,10 @@ export class DetailsCommercialPropertyListingComponent extends BaseDetailsCompon
|
||||
createEvent(eventType: EventTypeEnum) {
|
||||
this.auditService.createEvent(this.listing.id, eventType, this.user?.email);
|
||||
}
|
||||
getDaysListed() {
|
||||
return dayjs().diff(this.listing.created, 'day');
|
||||
}
|
||||
dateInserted() {
|
||||
return dayjs(this.listing.created).format('DD/MM/YYYY');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container mx-auto p-4">
|
||||
@if(user){
|
||||
<div class="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<div class="bg-white drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg rounded-lg overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b relative">
|
||||
<div class="flex items-center space-x-4">
|
||||
@@ -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,205 +1,252 @@
|
||||
<header class="w-full flex justify-between items-center p-4 bg-white fixed 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">
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white">
|
||||
<div class="container mx-auto px-6 py-3 flex justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
<a href="#" class="text-2xl font-bold text-blue-800">
|
||||
<img src="assets/images/header-logo.png" alt="BizMatch.net" class="h-10" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="#" class="text-gray-800 hover:text-blue-600">Home</a>
|
||||
<a routerLink="/businessListings" class="text-blue-700 hover:font-bold">Businesses</a>
|
||||
<a href="#services" class="text-gray-800 hover:text-blue-600">Services</a>
|
||||
<a href="#location" class="text-gray-800 hover:text-blue-600">Location</a>
|
||||
<a href="#contact" class="text-gray-800 hover:text-blue-600">Contact</a>
|
||||
@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="/logout" class="text-gray-800 hover:text-blue-600">Logout</a>
|
||||
}@else{
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="text-gray-800 hover:text-blue-600">Log In</a>
|
||||
}
|
||||
</div>
|
||||
<button (click)="toggleMenu()" class="md:hidden text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16m-7 6h7"></path>
|
||||
<div class="md:hidden">
|
||||
<button class="text-gray-800 focus:outline-none" (click)="toggleMobileMenu()">
|
||||
<svg class="h-6 w-6 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div *ngIf="isMenuOpen" class="fixed inset-0 bg-gray-800 bg-opacity-75 z-20">
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<!-- <a href="#" class="text-white text-xl py-2">Pricing</a> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile menu (only shows when toggleMobileMenu is true) -->
|
||||
<div *ngIf="showMobileMenu" class="md:hidden bg-white py-2 px-4">
|
||||
<a href="#" class="block py-2 text-gray-800 hover:text-blue-600">Home</a>
|
||||
<a href="#services" class="block py-2 text-gray-800 hover:text-blue-600">Services</a>
|
||||
<a href="#location" class="block py-2 text-gray-800 hover:text-blue-600">Location</a>
|
||||
<a href="#contact" class="block py-2 text-gray-800 hover:text-blue-600">Contact</a>
|
||||
@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="/logout" class="block py-2 text-gray-800 hover:text-blue-600">Logout</a>
|
||||
}@else{
|
||||
<a routerLink="/login" [queryParams]="{ mode: 'login' }" class="block py-2 text-gray-800 hover:text-blue-600">Log In</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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="flex flex-col items-center justify-center mt-16 md:mt-20 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 }">
|
||||
@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">
|
||||
<li class="me-2">
|
||||
<a
|
||||
(click)="changeTab('business')"
|
||||
[ngClass]="
|
||||
activeTabAction === 'business'
|
||||
? ['text-blue-600', 'border-blue-600', 'active', 'dark:text-blue-500', 'dark:border-blue-500']
|
||||
: ['border-transparent', 'hover:text-gray-600', 'hover:border-gray-300', 'dark:hover:text-gray-300']
|
||||
"
|
||||
class="hover:cursor-pointer inline-block p-4 border-b-2 rounded-t-lg"
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
<li class="me-2">
|
||||
<a
|
||||
(click)="changeTab('commercialProperty')"
|
||||
[ngClass]="
|
||||
activeTabAction === 'commercialProperty'
|
||||
? ['text-blue-600', 'border-blue-600', 'active', 'dark:text-blue-500', 'dark:border-blue-500']
|
||||
: ['border-transparent', 'hover:text-gray-600', 'hover:border-gray-300', 'dark:hover:text-gray-300']
|
||||
"
|
||||
class="hover:cursor-pointer inline-block p-4 border-b-2 rounded-t-lg"
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
<li class="me-2">
|
||||
<a
|
||||
(click)="changeTab('broker')"
|
||||
[ngClass]="
|
||||
activeTabAction === 'broker'
|
||||
? ['text-blue-600', 'border-blue-600', 'active', 'dark:text-blue-500', 'dark:border-blue-500']
|
||||
: ['border-transparent', 'hover:text-gray-600', 'hover:border-gray-300', 'dark:hover:text-gray-300']
|
||||
"
|
||||
class="hover:cursor-pointer inline-block p-4 border-b-2 rounded-t-lg"
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Hero Section (made narrower) -->
|
||||
<section class="hero-section flex items-center px-[2rem] py-[5rem]">
|
||||
<div class="container mx-auto px-6 flex flex-col">
|
||||
<!-- max-w-5xl makes it narrower -->
|
||||
<div class="flex flex-col md:flex-row items-center">
|
||||
<div class="md:w-1/2 text-white">
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold leading-tight mb-4">Connect with Your Ideal Business Opportunity</h1>
|
||||
<p class="text-xl mb-8">BizMatch is your trusted partner in buying, selling, and valuing businesses in Texas.</p>
|
||||
</div>
|
||||
} @if(aiSearch){
|
||||
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-gray-300">
|
||||
<div class="md:w-48 flex-1 md:border-r border-gray-300 overflow-hidden mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-gray-300 rounded-md">
|
||||
<input #aiSearchInput type="text" [(ngModel)]="aiSearchText" name="aiSearchText" class="w-full p-2 border border-gray-300 rounded-md" (focus)="stopTypingEffect()" (blur)="startTypingEffect()" />
|
||||
<div class="md:w-1/2 flex justify-center">
|
||||
<img src="assets/images/corpusChristiSkyline.jpg" alt="Business handshake" class="rounded-lg shadow-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-blue-600 hover:bg-blue-500 transition-colors duration-200 max-sm:rounded-md">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full h-full text-white font-semibold py-3 px-6 focus:outline-none rounded-md md:rounded-none flex items-center justify-center min-w-[180px] min-h-[48px]"
|
||||
(click)="generateAiResponse()"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
@if(loadingAi){
|
||||
<svg aria-hidden="true" role="status" class="w-4 h-4 mr-3 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<div class="flex justify-center mt-10">
|
||||
<a routerLink="/businessListings" class="bg-green-500 md:text-2xl text-lg text-white font-semibold px-8 py-4 rounded-full shadow-lg hover:bg-green-600 transition duration-300"> View Available Businesses </a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Services Section -->
|
||||
<section id="services" class="py-20 bg-gray-50">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl font-bold text-blue-800 mb-4">Our Services</h2>
|
||||
<p class="text-gray-600 max-w-2xl mx-auto">We offer comprehensive business brokerage services to help you navigate the complex process of buying or selling a business.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap -mx-4">
|
||||
<!-- Service 1 -->
|
||||
<div class="w-full md:w-1/3 px-4 mb-8">
|
||||
<div class="service-card bg-white rounded-lg filter md:drop-shadow-custom-bg drop-shadow-custom-bg-mobile p-8 h-full">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="#E5E7EB"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Loading ...</span>
|
||||
} @else {
|
||||
<span>Search</span>
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-4 text-center">Business Sales</h3>
|
||||
<p class="text-gray-600 text-center">We help business owners prepare and market their businesses to qualified buyers, ensuring confidentiality throughout the process.</p>
|
||||
</div>
|
||||
</div>
|
||||
@if(aiSearchFailed){
|
||||
<div id="error-message" class="w-full max-w-3xl mx-auto mt-2 text-red-600 text-center">Search timed out. Please try again or use classic Search</div>
|
||||
} } @if(criteria && !aiSearch){
|
||||
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-gray-300">
|
||||
<div class="md:flex-none md:w-48 flex-1 md:border-r border-gray-300 overflow-hidden mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-gray-300 rounded-md">
|
||||
<select
|
||||
class="appearance-none bg-transparent w-full py-3 px-4 pr-8 focus:outline-none md:border-none rounded-md md:rounded-none"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onTypesChange($event)"
|
||||
[ngClass]="{ 'placeholder-selected': criteria.types.length === 0 }"
|
||||
>
|
||||
<option [value]="[]">{{ getPlaceholderLabel() }}</option>
|
||||
@for(type of getTypes(); track type){
|
||||
<option [value]="type.value">{{ type.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<i class="fas fa-chevron-down text-xs"></i>
|
||||
|
||||
<!-- Service 2 -->
|
||||
<div class="w-full md:w-1/3 px-4 mb-8">
|
||||
<div class="service-card bg-white rounded-lg filter md:drop-shadow-custom-bg drop-shadow-custom-bg-mobile p-8 h-full">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-4 text-center">Business Acquisitions</h3>
|
||||
<p class="text-gray-600 text-center">We assist buyers in finding the right business opportunity, perform due diligence, and negotiate favorable terms for acquisition.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Service 3 -->
|
||||
<div class="w-full md:w-1/3 px-4 mb-8">
|
||||
<div class="service-card bg-white rounded-lg filter md:drop-shadow-custom-bg drop-shadow-custom-bg-mobile p-8 h-full">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 9.134a1 1 0 010 1.732l-3.354 1.935-1.18 4.455a1 1 0 01-1.933 0L9.854 12.8 6.5 10.866a1 1 0 010-1.732l3.354-1.935 1.18-4.455A1 1 0 0112 2z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-4 text-center">Business Valuation</h3>
|
||||
<p class="text-gray-600 text-center">Our expert team provides accurate business valuations based on industry standards, financial performance, and market conditions.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:flex-auto md:w-36 flex-grow md:border-r border-gray-300 mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-gray-300 rounded-md">
|
||||
<ng-select
|
||||
class="custom md:border-none rounded-md md:rounded-none"
|
||||
[multiple]="false"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="cityLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="cityInput$"
|
||||
[ngModel]="cityOrState"
|
||||
(ngModelChange)="setCityOrState($event)"
|
||||
placeholder="Enter City or State ..."
|
||||
groupBy="type"
|
||||
<!-- Video Section -->
|
||||
<div class="mt-16 text-center">
|
||||
<h3 class="text-2xl font-semibold text-blue-800 mb-8">See How We Work</h3>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<video controls class="w-full rounded-lg shadow-xl" poster="assets/images/video-poster.png">
|
||||
<source src="assets/videos/Bizmatch30Spot.mp4" type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Why Choose Us Section -->
|
||||
<section class="py-20 bg-white">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl font-bold text-blue-800 mb-4">Why Choose BizMatch</h2>
|
||||
<p class="text-gray-600 max-w-2xl mx-auto">With decades of experience in the business brokerage industry, we provide unparalleled service to our clients.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap -mx-4">
|
||||
<!-- Feature 1 -->
|
||||
<div class="w-full md:w-1/4 px-4 mb-8">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6 mx-auto">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 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"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-2">Experience</h3>
|
||||
<p class="text-gray-600">Over 25 years of combined experience in business brokerage.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="w-full md:w-1/4 px-4 mb-8">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-6 mx-auto">
|
||||
<svg class="w-8 h-8 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<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"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-2">Confidentiality</h3>
|
||||
<p class="text-gray-600">We maintain strict confidentiality throughout the entire transaction process.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3 -->
|
||||
<div class="w-full md:w-1/4 px-4 mb-8">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-6 mx-auto">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-2">Network</h3>
|
||||
<p class="text-gray-600">Extensive network of qualified buyers and business owners throughout Texas.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feature 4 -->
|
||||
<div class="w-full md:w-1/4 px-4 mb-8">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mb-6 mx-auto">
|
||||
<svg class="w-8 h-8 text-yellow-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-2">Personalized Approach</h3>
|
||||
<p class="text-gray-600">Customized strategy for each client based on their unique business goals.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Location Section -->
|
||||
<section id="location" class="py-20 bg-gray-50">
|
||||
<div class="container mx-auto px-6">
|
||||
<div class="flex flex-wrap items-stretch">
|
||||
<!-- Changed from items-center to items-stretch -->
|
||||
<div class="w-full lg:w-2/5 mb-12 lg:mb-0">
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Added flex container with h-full -->
|
||||
<h2 class="text-3xl font-bold text-blue-800 mb-6">Visit Our Office</h2>
|
||||
<p class="text-gray-600 mb-8 text-lg">Our team of business brokers is ready to assist you at our Corpus Christi location.</p>
|
||||
<div class="bg-white p-6 rounded-lg shadow-lg flex-grow">
|
||||
<!-- Added flex-grow to make it fill available space -->
|
||||
<h3 class="text-xl font-semibold text-blue-800 mb-4">BizMatch Headquarters</h3>
|
||||
<p class="text-gray-600 mb-2">1001 Blucher Street</p>
|
||||
<p class="text-gray-600 mb-2">Corpus Christi, TX 78401</p>
|
||||
<p class="text-gray-600 mb-6">United States</p>
|
||||
<p class="text-gray-600 mb-2"><strong>Phone:</strong> (555) 123-4567</p>
|
||||
<p class="text-gray-600"><strong>Email:</strong> info@bizmatch.net</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:w-3/5">
|
||||
<div class="rounded-lg overflow-hidden shadow-xl h-full min-h-[384px]">
|
||||
<!-- Changed h-96 to h-full with min-height -->
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3533.7894679685755!2d-97.38527228476843!3d27.773756032788047!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x866c1e3b8a9d0c0b%3A0x8f2c1d4c1a5c5b2c!2s1001%20Blucher%20St%2C%20Corpus%20Christi%2C%20TX%2078401%2C%20USA!5e0!3m2!1sen!2sde!4v1672531192743!5m2!1sen!2sde"
|
||||
width="100%"
|
||||
height="100%"
|
||||
class="rounded-lg border-0"
|
||||
style="min-height: 384px; display: block"
|
||||
allowfullscreen=""
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
>
|
||||
@for (city of cities$ | async; track city.id) { @let state = city.type==='city'?city.content.state:''; @let separator = city.type==='city'?' - ':'';
|
||||
<ng-option [value]="city">{{ city.content.name }}{{ separator }}{{ state }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
</div>
|
||||
@if (criteria.radius && !aiSearch){
|
||||
<div class="md:flex-none md:w-36 flex-1 md:border-r border-gray-300 mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-gray-300 rounded-md">
|
||||
<select
|
||||
class="appearance-none bg-transparent w-full py-3 px-4 pr-8 focus:outline-none md:border-none rounded-md md:rounded-none"
|
||||
(ngModelChange)="onRadiusChange($event)"
|
||||
[ngModel]="criteria.radius"
|
||||
[ngClass]="{ 'placeholder-selected': !criteria.radius }"
|
||||
>
|
||||
<option [value]="null">City Radius</option>
|
||||
@for(dist of selectOptions.distances; track dist){
|
||||
<option [value]="dist.value">{{ dist.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<i class="fas fa-chevron-down text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="bg-blue-600 hover:bg-blue-500 transition-colors duration-200 max-sm:rounded-md">
|
||||
@if(getNumberOfFiltersSet()>0 && numberOfResults$){
|
||||
<button class="w-full h-full text-white font-semibold py-3 px-6 focus:outline-none rounded-md md:rounded-none" (click)="search()">Search ({{ numberOfResults$ | async }})</button>
|
||||
}@else {
|
||||
<button class="w-full h-full text-white font-semibold py-3 px-6 focus:outline-none rounded-md md:rounded-none" (click)="search()">Search</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<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>
|
||||
<span class="ml-2">- Try now</span>
|
||||
<div class="ml-4 relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
||||
<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>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section id="contact" class="py-20 bg-blue-700">
|
||||
<div class="container mx-auto px-6 text-center">
|
||||
<h2 class="text-3xl font-bold text-white mb-8">Ready to Get Started?</h2>
|
||||
<p class="text-white text-xl mb-12 max-w-3xl mx-auto">Contact our team of experienced business brokers today for a confidential consultation about buying or selling a business.</p>
|
||||
<a routerLink="/emailUs" class="bg-white text-blue-700 font-bold px-8 py-4 rounded-lg shadow-lg hover:bg-gray-100 transition duration-300 text-lg">Contact Us Now</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
@@ -1,74 +1,85 @@
|
||||
.bg-cover-custom {
|
||||
background-image: url('/assets/images/index-bg.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.3);
|
||||
min-height: calc(100vh - 4rem);
|
||||
// Hero section styles
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, #0046b5 0%, #00a0e9 100%);
|
||||
// height: 70vh; // Made shorter as requested
|
||||
// min-height: 500px; // Reduced from 600px
|
||||
}
|
||||
select:not([size]) {
|
||||
background-image: unset;
|
||||
}
|
||||
[type='text'],
|
||||
[type='email'],
|
||||
[type='url'],
|
||||
[type='password'],
|
||||
[type='number'],
|
||||
[type='date'],
|
||||
[type='datetime-local'],
|
||||
[type='month'],
|
||||
[type='search'],
|
||||
[type='tel'],
|
||||
[type='time'],
|
||||
[type='week'],
|
||||
[multiple],
|
||||
textarea,
|
||||
select {
|
||||
border: unset;
|
||||
}
|
||||
.toggle-checkbox:checked {
|
||||
right: 0;
|
||||
border-color: rgb(125 211 252);
|
||||
}
|
||||
.toggle-checkbox:checked + .toggle-label {
|
||||
background-color: rgb(125 211 252);
|
||||
}
|
||||
:host ::ng-deep .ng-select.ng-select-single .ng-select-container {
|
||||
height: 48px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
.ng-value-container .ng-input {
|
||||
top: 10px;
|
||||
}
|
||||
span.ng-arrow-wrapper {
|
||||
display: none;
|
||||
|
||||
// Button hover effects
|
||||
.btn-primary {
|
||||
background-color: #0046b5;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #003492;
|
||||
}
|
||||
}
|
||||
select {
|
||||
color: #000; /* Standard-Textfarbe für das Dropdown */
|
||||
// background-color: #fff; /* Hintergrundfarbe für das Dropdown */
|
||||
|
||||
// Service card animation
|
||||
.service-card {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
select option {
|
||||
color: #000; /* Textfarbe für Dropdown-Optionen */
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
height: auto;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
select.placeholder-selected {
|
||||
color: #999; /* Farbe für den Platzhalter */
|
||||
}
|
||||
input::placeholder {
|
||||
color: #555; /* Dunkleres Grau */
|
||||
opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */
|
||||
// Make sure the Google Map is responsive
|
||||
google-map {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */
|
||||
select:focus option,
|
||||
select:hover option {
|
||||
color: #000 !important;
|
||||
// Override Tailwind default styling for video
|
||||
video {
|
||||
max-width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
input[type='text'][name='aiSearchText'] {
|
||||
padding: 14px; /* Innerer Abstand */
|
||||
font-size: 16px; /* Schriftgröße anpassen */
|
||||
box-sizing: border-box; /* Padding und Border in die Höhe und Breite einrechnen */
|
||||
height: 48px;
|
||||
|
||||
// Zusätzliche Styles für den Location-Bereich
|
||||
|
||||
// Verbesserte Map-Container Styles
|
||||
#location {
|
||||
.rounded-lg.overflow-hidden {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 384px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// Stellen Sie sicher, dass der Kartencontainer im mobilen Layout
|
||||
// eine angemessene Höhe hat
|
||||
@media (max-width: 1023px) {
|
||||
.rounded-lg.overflow-hidden {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adressbox-Styling verbessern
|
||||
.bg-white.p-6.rounded-lg.shadow-lg.flex-grow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
// Sicherstellen, dass der untere Bereich sichtbar bleibt
|
||||
.contact-info {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,316 +1,52 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
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 { CriteriaChangeService } from '../../services/criteria-change.service';
|
||||
import { GeoService } from '../../services/geo.service';
|
||||
import { ListingsService } from '../../services/listings.service';
|
||||
import { SearchService } from '../../services/search.service';
|
||||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RouterLink, RouterOutlet } from '@angular/router';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import {
|
||||
assignProperties,
|
||||
compareObjects,
|
||||
createEmptyBusinessListingCriteria,
|
||||
createEmptyCommercialPropertyListingCriteria,
|
||||
createEmptyUserListingCriteria,
|
||||
createEnhancedProxy,
|
||||
getCriteriaStateObject,
|
||||
map2User,
|
||||
} from '../../utils/utils';
|
||||
@UntilDestroy()
|
||||
import { map2User } from '../../utils/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterModule, NgSelectModule, TooltipComponent],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, RouterLink],
|
||||
})
|
||||
export class HomeComponent {
|
||||
placeholders: string[] = ['Property close to Houston less than 10M', 'Franchise business in Austin price less than 500K'];
|
||||
activeTabAction: 'business' | 'commercialProperty' | 'broker' = 'business';
|
||||
type: string;
|
||||
maxPrice: string;
|
||||
minPrice: string;
|
||||
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
|
||||
states = [];
|
||||
isMenuOpen = false;
|
||||
user: KeycloakUser;
|
||||
prompt: string;
|
||||
cities$: Observable<CityAndStateResult[]>;
|
||||
cityLoading = false;
|
||||
cityInput$ = new Subject<string>();
|
||||
cityOrState = undefined;
|
||||
private criteriaChangeSubscription: Subscription;
|
||||
numberOfResults$: Observable<number>;
|
||||
export class HomeComponent implements OnInit {
|
||||
showMobileMenu = false;
|
||||
keycloakUser: KeycloakUser;
|
||||
user: User;
|
||||
constructor(private authService: AuthService, private userService: UserService) {}
|
||||
|
||||
aiSearch = false;
|
||||
aiSearchText = '';
|
||||
aiSearchFailed = false;
|
||||
loadingAi = false;
|
||||
@ViewChild('aiSearchInput', { static: false }) searchInput!: ElementRef;
|
||||
typingSpeed: number = 100; // Geschwindigkeit des Tippens (ms)
|
||||
pauseTime: number = 2000; // Pausezeit, bevor der Text verschwindet (ms)
|
||||
index: number = 0;
|
||||
charIndex: number = 0;
|
||||
typingInterval: any;
|
||||
showInput: boolean = true; // Steuerung der Anzeige des Eingabefelds
|
||||
tooltipTargetBeta = 'tooltipTargetBeta';
|
||||
public constructor(
|
||||
private router: Router,
|
||||
private modalService: ModalService,
|
||||
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,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 0);
|
||||
const token = await this.keycloakService.getToken();
|
||||
sessionStorage.removeItem('businessListings');
|
||||
sessionStorage.removeItem('commercialPropertyListings');
|
||||
sessionStorage.removeItem('brokerListings');
|
||||
this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this);
|
||||
this.user = map2User(token);
|
||||
this.loadCities();
|
||||
this.setupCriteriaChangeListener();
|
||||
}
|
||||
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
|
||||
this.activeTabAction = tabname;
|
||||
this.cityOrState = null;
|
||||
if ('business' === tabname) {
|
||||
this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this);
|
||||
} else if ('commercialProperty' === tabname) {
|
||||
this.criteria = createEnhancedProxy(getCriteriaStateObject('commercialPropertyListings'), this);
|
||||
} else if ('broker' === tabname) {
|
||||
this.criteria = createEnhancedProxy(getCriteriaStateObject('brokerListings'), this);
|
||||
} else {
|
||||
this.criteria = undefined;
|
||||
// Add smooth scrolling for anchor links
|
||||
this.setupSmoothScrolling();
|
||||
const token = await this.authService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
if (this.keycloakUser) {
|
||||
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||
this.userService.changeUser(this.user);
|
||||
}
|
||||
}
|
||||
|
||||
search() {
|
||||
this.router.navigate([`${this.activeTabAction}Listings`]);
|
||||
toggleMobileMenu(): void {
|
||||
this.showMobileMenu = !this.showMobileMenu;
|
||||
}
|
||||
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}`,
|
||||
|
||||
private setupSmoothScrolling(): void {
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector((this as HTMLAnchorElement).getAttribute('href') || '');
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
register() {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||
}
|
||||
toggleMenu() {
|
||||
this.isMenuOpen = !this.isMenuOpen;
|
||||
}
|
||||
onTypesChange(value) {
|
||||
if (value === '') {
|
||||
// Wenn keine Option ausgewählt ist, setzen Sie types zurück auf ein leeres Array
|
||||
this.criteria.types = [];
|
||||
} else {
|
||||
this.criteria.types = [value];
|
||||
}
|
||||
}
|
||||
onRadiusChange(value) {
|
||||
if (value === 'null') {
|
||||
// Wenn keine Option ausgewählt ist, setzen Sie types zurück auf ein leeres Array
|
||||
this.criteria.radius = null;
|
||||
} else {
|
||||
this.criteria.radius = parseInt(value);
|
||||
}
|
||||
}
|
||||
async openModal() {
|
||||
const accepted = await this.modalService.showModal(this.criteria);
|
||||
if (accepted) {
|
||||
this.router.navigate([`${this.activeTabAction}Listings`]);
|
||||
}
|
||||
}
|
||||
private loadCities() {
|
||||
this.cities$ = concat(
|
||||
of([]), // default items
|
||||
this.cityInput$.pipe(
|
||||
distinctUntilChanged(),
|
||||
tap(() => (this.cityLoading = true)),
|
||||
switchMap(term =>
|
||||
//this.geoService.findCitiesStartingWith(term).pipe(
|
||||
this.geoService.findCitiesAndStatesStartingWith(term).pipe(
|
||||
catchError(() => of([])), // empty list on error
|
||||
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
|
||||
tap(() => (this.cityLoading = false)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
trackByFn(item: GeoResult) {
|
||||
return item.id;
|
||||
}
|
||||
setCityOrState(cityOrState: CityAndStateResult) {
|
||||
if (cityOrState) {
|
||||
if (cityOrState.type === 'state') {
|
||||
this.criteria.state = cityOrState.content.state_code;
|
||||
} else {
|
||||
this.criteria.city = cityOrState.content as GeoResult;
|
||||
this.criteria.state = cityOrState.content.state;
|
||||
this.criteria.searchType = 'radius';
|
||||
this.criteria.radius = 20;
|
||||
}
|
||||
} else {
|
||||
this.criteria.state = null;
|
||||
this.criteria.city = null;
|
||||
this.criteria.radius = null;
|
||||
this.criteria.searchType = 'exact';
|
||||
}
|
||||
}
|
||||
getTypes() {
|
||||
if (this.criteria.criteriaType === 'businessListings') {
|
||||
return this.selectOptions.typesOfBusiness;
|
||||
} else if (this.criteria.criteriaType === 'commercialPropertyListings') {
|
||||
return this.selectOptions.typesOfCommercialProperty;
|
||||
} else {
|
||||
return this.selectOptions.customerSubTypes;
|
||||
}
|
||||
}
|
||||
getPlaceholderLabel() {
|
||||
if (this.criteria.criteriaType === 'businessListings') {
|
||||
return 'Business Type';
|
||||
} else if (this.criteria.criteriaType === 'commercialPropertyListings') {
|
||||
return 'Property Type';
|
||||
} else {
|
||||
return 'Professional Type';
|
||||
}
|
||||
}
|
||||
setTotalNumberOfResults() {
|
||||
if (this.criteria) {
|
||||
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
|
||||
if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') {
|
||||
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty');
|
||||
} else if (this.criteria.criteriaType === 'brokerListings') {
|
||||
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
|
||||
} else {
|
||||
this.numberOfResults$ = of();
|
||||
}
|
||||
}
|
||||
}
|
||||
getNumberOfFiltersSet() {
|
||||
if (this.criteria?.criteriaType === 'brokerListings') {
|
||||
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||
} else if (this.criteria?.criteriaType === 'businessListings') {
|
||||
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||
} else if (this.criteria?.criteriaType === 'commercialPropertyListings') {
|
||||
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
toggleAiSearch() {
|
||||
this.aiSearch = !this.aiSearch;
|
||||
this.aiSearchFailed = false;
|
||||
if (!this.aiSearch) {
|
||||
this.aiSearchText = '';
|
||||
this.stopTypingEffect();
|
||||
} else {
|
||||
setTimeout(() => this.startTypingEffect(), 0);
|
||||
}
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
clearTimeout(this.typingInterval); // Stelle sicher, dass das Intervall gestoppt wird, wenn die Komponente zerstört wird
|
||||
}
|
||||
|
||||
startTypingEffect(): void {
|
||||
if (!this.aiSearchText) {
|
||||
this.typePlaceholder();
|
||||
}
|
||||
}
|
||||
|
||||
stopTypingEffect(): void {
|
||||
clearTimeout(this.typingInterval);
|
||||
}
|
||||
typePlaceholder(): void {
|
||||
if (!this.searchInput || !this.searchInput.nativeElement) {
|
||||
return; // Falls das Eingabefeld nicht verfügbar ist (z.B. durch ngIf)
|
||||
}
|
||||
|
||||
if (this.aiSearchText) {
|
||||
return; // Stoppe, wenn der Benutzer Text eingegeben hat
|
||||
}
|
||||
|
||||
const inputField = this.searchInput.nativeElement as HTMLInputElement;
|
||||
if (document.activeElement === inputField) {
|
||||
this.stopTypingEffect();
|
||||
return;
|
||||
}
|
||||
|
||||
inputField.placeholder = this.placeholders[this.index].substring(0, this.charIndex);
|
||||
|
||||
if (this.charIndex < this.placeholders[this.index].length) {
|
||||
this.charIndex++;
|
||||
this.typingInterval = setTimeout(() => this.typePlaceholder(), this.typingSpeed);
|
||||
} else {
|
||||
// Nach dem vollständigen Tippen eine Pause einlegen
|
||||
this.typingInterval = setTimeout(() => {
|
||||
inputField.placeholder = ''; // Schlagartiges Löschen des Platzhalters
|
||||
this.charIndex = 0;
|
||||
this.index = (this.index + 1) % this.placeholders.length;
|
||||
this.typingInterval = setTimeout(() => this.typePlaceholder(), this.typingSpeed);
|
||||
}, this.pauseTime);
|
||||
}
|
||||
}
|
||||
async generateAiResponse() {
|
||||
this.loadingAi = true;
|
||||
this.aiSearchFailed = false;
|
||||
try {
|
||||
const result = await this.aiService.generateAiReponse(this.aiSearchText);
|
||||
let criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria | any;
|
||||
if (result.criteriaType === 'businessListings') {
|
||||
this.changeTab('business');
|
||||
criteria = result as BusinessListingCriteria;
|
||||
} else if (result.criteriaType === 'commercialPropertyListings') {
|
||||
this.changeTab('commercialProperty');
|
||||
criteria = result as CommercialPropertyListingCriteria;
|
||||
} else {
|
||||
this.changeTab('broker');
|
||||
criteria = result as UserListingCriteria;
|
||||
}
|
||||
const city = criteria.city as string;
|
||||
if (city && city.length > 0) {
|
||||
let results = await lastValueFrom(this.geoService.findCitiesStartingWith(city, criteria.state));
|
||||
if (results.length > 0) {
|
||||
criteria.city = results[0];
|
||||
} else {
|
||||
criteria.city = null;
|
||||
}
|
||||
}
|
||||
if (criteria.radius && criteria.radius.length > 0) {
|
||||
criteria.radius = parseInt(criteria.radius);
|
||||
}
|
||||
this.loadingAi = false;
|
||||
this.criteria = assignProperties(this.criteria, criteria);
|
||||
this.search();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this.aiSearchFailed = true;
|
||||
this.loadingAi = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
@if(users?.length>0){
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Amanda Taylor -->
|
||||
@for (user of users; track user) {
|
||||
<div class="bg-white rounded-lg shadow-md p-6 flex flex-col justify-between">
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6 flex flex-col justify-between">
|
||||
<div class="flex items-start space-x-4">
|
||||
@if(user.hasProfile){
|
||||
<img src="{{ env.imageBaseUrl }}/pictures/profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="rounded-md w-20 h-26 object-cover" />
|
||||
@@ -36,6 +37,60 @@
|
||||
|
||||
}
|
||||
</div>
|
||||
} @else if (users?.length===0){
|
||||
<div class="w-full flex items-center flex-wrap justify-center gap-10">
|
||||
<div class="grid gap-4 w-60">
|
||||
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
|
||||
<path
|
||||
d="M0.0616455 84.4268C0.0616455 42.0213 34.435 7.83765 76.6507 7.83765C118.803 7.83765 153.224 42.0055 153.224 84.4268C153.224 102.42 147.026 118.974 136.622 132.034C122.282 150.138 100.367 161 76.6507 161C52.7759 161 30.9882 150.059 16.6633 132.034C6.25961 118.974 0.0616455 102.42 0.0616455 84.4268Z"
|
||||
fill="#EEF2FF"
|
||||
/>
|
||||
<path
|
||||
d="M96.8189 0.632498L96.8189 0.632384L96.8083 0.630954C96.2034 0.549581 95.5931 0.5 94.9787 0.5H29.338C22.7112 0.5 17.3394 5.84455 17.3394 12.4473V142.715C17.3394 149.318 22.7112 154.662 29.338 154.662H123.948C130.591 154.662 135.946 149.317 135.946 142.715V38.9309C135.946 38.0244 135.847 37.1334 135.648 36.2586L135.648 36.2584C135.117 33.9309 133.874 31.7686 132.066 30.1333C132.066 30.1331 132.065 30.1329 132.065 30.1327L103.068 3.65203C103.068 3.6519 103.067 3.65177 103.067 3.65164C101.311 2.03526 99.1396 0.995552 96.8189 0.632498Z"
|
||||
fill="white"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<ellipse cx="80.0618" cy="81" rx="28.0342" ry="28.0342" fill="#EEF2FF" />
|
||||
<path
|
||||
d="M99.2393 61.3061L99.2391 61.3058C88.498 50.5808 71.1092 50.5804 60.3835 61.3061C49.6423 72.0316 49.6422 89.4361 60.3832 100.162C71.109 110.903 88.4982 110.903 99.2393 100.162C109.965 89.4363 109.965 72.0317 99.2393 61.3061ZM105.863 54.6832C120.249 69.0695 120.249 92.3985 105.863 106.785C91.4605 121.171 68.1468 121.171 53.7446 106.785C39.3582 92.3987 39.3582 69.0693 53.7446 54.683C68.1468 40.2965 91.4605 40.2966 105.863 54.6832Z"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<path d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z" stroke="#E5E7EB" />
|
||||
<path
|
||||
d="M139.122 125.781L139.122 125.78L123.313 109.988C123.313 109.987 123.313 109.987 123.312 109.986C121.996 108.653 119.849 108.657 118.521 109.985L118.871 110.335L118.521 109.985L109.047 119.459C107.731 120.775 107.735 122.918 109.044 124.247L109.047 124.249L124.858 140.06C128.789 143.992 135.191 143.992 139.122 140.06C143.069 136.113 143.069 129.728 139.122 125.781Z"
|
||||
fill="#A5B4FC"
|
||||
stroke="#818CF8"
|
||||
/>
|
||||
<path
|
||||
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 83.3821 77.9987 83.3821 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<path
|
||||
d="M93.3528 77.0926H88.403C87.7409 77.0926 87.2049 76.5567 87.2049 75.8946C87.2049 75.2483 87.7409 74.7123 88.403 74.7123H93.3528C94.0149 74.7123 94.5509 75.2483 94.5509 75.8946C94.5509 76.5567 94.0149 77.0926 93.3528 77.0926Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<path
|
||||
d="M71.5987 77.0925H66.6488C65.9867 77.0925 65.4507 76.5565 65.4507 75.8945C65.4507 75.2481 65.9867 74.7122 66.6488 74.7122H71.5987C72.245 74.7122 72.781 75.2481 72.781 75.8945C72.781 76.5565 72.245 77.0925 71.5987 77.0925Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<rect x="38.3522" y="21.5128" width="41.0256" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
<rect x="38.3522" y="133.65" width="54.7009" height="5.47009" rx="2.73504" fill="#A5B4FC" />
|
||||
<rect x="38.3522" y="29.7179" width="13.6752" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
<circle cx="56.13" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
<circle cx="61.6001" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
<circle cx="67.0702" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">There’re no professionals here</h2>
|
||||
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to <br />see professionals</p>
|
||||
<div class="flex gap-3">
|
||||
<button (click)="clearAllFilters()" class="w-full px-3 py-2 rounded-full border border-gray-300 text-gray-900 text-xs font-semibold leading-4">Clear Filter</button>
|
||||
<button (click)="openFilterModal()" class="w-full px-3 py-2 bg-indigo-600 hover:bg-indigo-700 transition-all duration-500 rounded-full text-white text-xs font-semibold leading-4">Change Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if(pageCount>1){
|
||||
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
|
||||
|
||||
@@ -8,12 +8,14 @@ import { LISTINGS_PER_PAGE, ListingType, UserListingCriteria, emailToDirName } f
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { CustomerSubTypeComponent } from '../../../components/customer-sub-type/customer-sub-type.component';
|
||||
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
|
||||
import { ModalService } from '../../../components/search-modal/modal.service';
|
||||
import { CriteriaChangeService } from '../../../services/criteria-change.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
import { SearchService } from '../../../services/search.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { UserService } from '../../../services/user.service';
|
||||
import { getCriteriaProxy } from '../../../utils/utils';
|
||||
import { assignProperties, getCriteriaProxy, resetUserListingCriteria } from '../../../utils/utils';
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-broker-listings',
|
||||
@@ -53,6 +55,8 @@ export class BrokerListingsComponent {
|
||||
private imageService: ImageService,
|
||||
private route: ActivatedRoute,
|
||||
private searchService: SearchService,
|
||||
private modalService: ModalService,
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
) {
|
||||
this.criteria = getCriteriaProxy('brokerListings', this) as UserListingCriteria;
|
||||
this.init();
|
||||
@@ -84,4 +88,29 @@ export class BrokerListingsComponent {
|
||||
}
|
||||
|
||||
reset() {}
|
||||
|
||||
// New methods for filter actions
|
||||
clearAllFilters() {
|
||||
// Reset criteria to default values
|
||||
resetUserListingCriteria(this.criteria);
|
||||
|
||||
// Reset pagination
|
||||
this.criteria.page = 1;
|
||||
this.criteria.start = 0;
|
||||
|
||||
this.criteriaChangeService.notifyCriteriaChange();
|
||||
|
||||
// Search with cleared filters
|
||||
this.searchService.search(this.criteria);
|
||||
}
|
||||
|
||||
async openFilterModal() {
|
||||
// Open the search modal with current criteria
|
||||
const modalResult = await this.modalService.showModal(this.criteria);
|
||||
if (modalResult.accepted) {
|
||||
this.searchService.search(this.criteria);
|
||||
} else {
|
||||
this.criteria = assignProperties(this.criteria, modalResult.criteria);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<div class="container mx-auto p-4">
|
||||
@if(listings?.length>0){
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Anzahl der Spalten auf 3 reduziert und den Abstand erhöht -->
|
||||
@for (listing of listings; track listing.id) {
|
||||
<div class="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl">
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden hover:shadow-xl">
|
||||
<!-- Hover-Effekt hinzugefügt -->
|
||||
<div class="p-6 flex flex-col h-full relative z-[0]">
|
||||
<div class="flex items-center mb-4">
|
||||
@@ -51,6 +52,60 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (listings?.length===0){
|
||||
<div class="w-full flex items-center flex-wrap justify-center gap-10">
|
||||
<div class="grid gap-4 w-60">
|
||||
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
|
||||
<path
|
||||
d="M0.0616455 84.4268C0.0616455 42.0213 34.435 7.83765 76.6507 7.83765C118.803 7.83765 153.224 42.0055 153.224 84.4268C153.224 102.42 147.026 118.974 136.622 132.034C122.282 150.138 100.367 161 76.6507 161C52.7759 161 30.9882 150.059 16.6633 132.034C6.25961 118.974 0.0616455 102.42 0.0616455 84.4268Z"
|
||||
fill="#EEF2FF"
|
||||
/>
|
||||
<path
|
||||
d="M96.8189 0.632498L96.8189 0.632384L96.8083 0.630954C96.2034 0.549581 95.5931 0.5 94.9787 0.5H29.338C22.7112 0.5 17.3394 5.84455 17.3394 12.4473V142.715C17.3394 149.318 22.7112 154.662 29.338 154.662H123.948C130.591 154.662 135.946 149.317 135.946 142.715V38.9309C135.946 38.0244 135.847 37.1334 135.648 36.2586L135.648 36.2584C135.117 33.9309 133.874 31.7686 132.066 30.1333C132.066 30.1331 132.065 30.1329 132.065 30.1327L103.068 3.65203C103.068 3.6519 103.067 3.65177 103.067 3.65164C101.311 2.03526 99.1396 0.995552 96.8189 0.632498Z"
|
||||
fill="white"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<ellipse cx="80.0618" cy="81" rx="28.0342" ry="28.0342" fill="#EEF2FF" />
|
||||
<path
|
||||
d="M99.2393 61.3061L99.2391 61.3058C88.498 50.5808 71.1092 50.5804 60.3835 61.3061C49.6423 72.0316 49.6422 89.4361 60.3832 100.162C71.109 110.903 88.4982 110.903 99.2393 100.162C109.965 89.4363 109.965 72.0317 99.2393 61.3061ZM105.863 54.6832C120.249 69.0695 120.249 92.3985 105.863 106.785C91.4605 121.171 68.1468 121.171 53.7446 106.785C39.3582 92.3987 39.3582 69.0693 53.7446 54.683C68.1468 40.2965 91.4605 40.2966 105.863 54.6832Z"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<path d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z" stroke="#E5E7EB" />
|
||||
<path
|
||||
d="M139.122 125.781L139.122 125.78L123.313 109.988C123.313 109.987 123.313 109.987 123.312 109.986C121.996 108.653 119.849 108.657 118.521 109.985L118.871 110.335L118.521 109.985L109.047 119.459C107.731 120.775 107.735 122.918 109.044 124.247L109.047 124.249L124.858 140.06C128.789 143.992 135.191 143.992 139.122 140.06C143.069 136.113 143.069 129.728 139.122 125.781Z"
|
||||
fill="#A5B4FC"
|
||||
stroke="#818CF8"
|
||||
/>
|
||||
<path
|
||||
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 83.3821 77.9987 83.3821 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<path
|
||||
d="M93.3528 77.0926H88.403C87.7409 77.0926 87.2049 76.5567 87.2049 75.8946C87.2049 75.2483 87.7409 74.7123 88.403 74.7123H93.3528C94.0149 74.7123 94.5509 75.2483 94.5509 75.8946C94.5509 76.5567 94.0149 77.0926 93.3528 77.0926Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<path
|
||||
d="M71.5987 77.0925H66.6488C65.9867 77.0925 65.4507 76.5565 65.4507 75.8945C65.4507 75.2481 65.9867 74.7122 66.6488 74.7122H71.5987C72.245 74.7122 72.781 75.2481 72.781 75.8945C72.781 76.5565 72.245 77.0925 71.5987 77.0925Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<rect x="38.3522" y="21.5128" width="41.0256" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
<rect x="38.3522" y="133.65" width="54.7009" height="5.47009" rx="2.73504" fill="#A5B4FC" />
|
||||
<rect x="38.3522" y="29.7179" width="13.6752" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
<circle cx="56.13" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
<circle cx="61.6001" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
<circle cx="67.0702" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">There’s no listing here</h2>
|
||||
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to <br />see listings</p>
|
||||
<div class="flex gap-3">
|
||||
<button (click)="clearAllFilters()" class="w-full px-3 py-2 rounded-full border border-gray-300 text-gray-900 text-xs font-semibold leading-4">Clear Filter</button>
|
||||
<button (click)="openFilterModal()" class="w-full px-3 py-2 bg-indigo-600 hover:bg-indigo-700 transition-all duration-500 rounded-full text-white text-xs font-semibold leading-4">Change Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(pageCount>1){
|
||||
|
||||
@@ -8,11 +8,13 @@ import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db
|
||||
import { BusinessListingCriteria, LISTINGS_PER_PAGE, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
|
||||
import { ModalService } from '../../../components/search-modal/modal.service';
|
||||
import { CriteriaChangeService } from '../../../services/criteria-change.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
import { SearchService } from '../../../services/search.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { getCriteriaProxy } from '../../../utils/utils';
|
||||
import { assignProperties, getCriteriaProxy, resetBusinessListingCriteria } from '../../../utils/utils';
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-business-listings',
|
||||
@@ -49,6 +51,8 @@ export class BusinessListingsComponent {
|
||||
private imageService: ImageService,
|
||||
private route: ActivatedRoute,
|
||||
private searchService: SearchService,
|
||||
private modalService: ModalService,
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
) {
|
||||
this.criteria = getCriteriaProxy('businessListings', this) as BusinessListingCriteria;
|
||||
this.init();
|
||||
@@ -88,4 +92,28 @@ export class BusinessListingsComponent {
|
||||
getDaysListed(listing: BusinessListing) {
|
||||
return dayjs().diff(listing.created, 'day');
|
||||
}
|
||||
// New methods for filter actions
|
||||
clearAllFilters() {
|
||||
// Reset criteria to default values
|
||||
resetBusinessListingCriteria(this.criteria);
|
||||
|
||||
// Reset pagination
|
||||
this.criteria.page = 1;
|
||||
this.criteria.start = 0;
|
||||
|
||||
this.criteriaChangeService.notifyCriteriaChange();
|
||||
|
||||
// Search with cleared filters
|
||||
this.searchService.search(this.criteria);
|
||||
}
|
||||
|
||||
async openFilterModal() {
|
||||
// Open the search modal with current criteria
|
||||
const modalResult = await this.modalService.showModal(this.criteria);
|
||||
if (modalResult.accepted) {
|
||||
this.searchService.search(this.criteria);
|
||||
} else {
|
||||
this.criteria = assignProperties(this.criteria, modalResult.criteria);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
@if(listings?.length>0){
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@for (listing of listings; track listing.id) {
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden flex flex-col h-full">
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden flex flex-col h-full">
|
||||
@if (listing.imageOrder?.length>0){
|
||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}" alt="Image" class="w-full h-48 object-cover" />
|
||||
} @else {
|
||||
@@ -33,6 +34,60 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (listings?.length===0){
|
||||
<div class="w-full flex items-center flex-wrap justify-center gap-10">
|
||||
<div class="grid gap-4 w-60">
|
||||
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
|
||||
<path
|
||||
d="M0.0616455 84.4268C0.0616455 42.0213 34.435 7.83765 76.6507 7.83765C118.803 7.83765 153.224 42.0055 153.224 84.4268C153.224 102.42 147.026 118.974 136.622 132.034C122.282 150.138 100.367 161 76.6507 161C52.7759 161 30.9882 150.059 16.6633 132.034C6.25961 118.974 0.0616455 102.42 0.0616455 84.4268Z"
|
||||
fill="#EEF2FF"
|
||||
/>
|
||||
<path
|
||||
d="M96.8189 0.632498L96.8189 0.632384L96.8083 0.630954C96.2034 0.549581 95.5931 0.5 94.9787 0.5H29.338C22.7112 0.5 17.3394 5.84455 17.3394 12.4473V142.715C17.3394 149.318 22.7112 154.662 29.338 154.662H123.948C130.591 154.662 135.946 149.317 135.946 142.715V38.9309C135.946 38.0244 135.847 37.1334 135.648 36.2586L135.648 36.2584C135.117 33.9309 133.874 31.7686 132.066 30.1333C132.066 30.1331 132.065 30.1329 132.065 30.1327L103.068 3.65203C103.068 3.6519 103.067 3.65177 103.067 3.65164C101.311 2.03526 99.1396 0.995552 96.8189 0.632498Z"
|
||||
fill="white"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<ellipse cx="80.0618" cy="81" rx="28.0342" ry="28.0342" fill="#EEF2FF" />
|
||||
<path
|
||||
d="M99.2393 61.3061L99.2391 61.3058C88.498 50.5808 71.1092 50.5804 60.3835 61.3061C49.6423 72.0316 49.6422 89.4361 60.3832 100.162C71.109 110.903 88.4982 110.903 99.2393 100.162C109.965 89.4363 109.965 72.0317 99.2393 61.3061ZM105.863 54.6832C120.249 69.0695 120.249 92.3985 105.863 106.785C91.4605 121.171 68.1468 121.171 53.7446 106.785C39.3582 92.3987 39.3582 69.0693 53.7446 54.683C68.1468 40.2965 91.4605 40.2966 105.863 54.6832Z"
|
||||
stroke="#E5E7EB"
|
||||
/>
|
||||
<path d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z" stroke="#E5E7EB" />
|
||||
<path
|
||||
d="M139.122 125.781L139.122 125.78L123.313 109.988C123.313 109.987 123.313 109.987 123.312 109.986C121.996 108.653 119.849 108.657 118.521 109.985L118.871 110.335L118.521 109.985L109.047 119.459C107.731 120.775 107.735 122.918 109.044 124.247L109.047 124.249L124.858 140.06C128.789 143.992 135.191 143.992 139.122 140.06C143.069 136.113 143.069 129.728 139.122 125.781Z"
|
||||
fill="#A5B4FC"
|
||||
stroke="#818CF8"
|
||||
/>
|
||||
<path
|
||||
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 83.3821 77.9987 83.3821 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<path
|
||||
d="M93.3528 77.0926H88.403C87.7409 77.0926 87.2049 76.5567 87.2049 75.8946C87.2049 75.2483 87.7409 74.7123 88.403 74.7123H93.3528C94.0149 74.7123 94.5509 75.2483 94.5509 75.8946C94.5509 76.5567 94.0149 77.0926 93.3528 77.0926Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<path
|
||||
d="M71.5987 77.0925H66.6488C65.9867 77.0925 65.4507 76.5565 65.4507 75.8945C65.4507 75.2481 65.9867 74.7122 66.6488 74.7122H71.5987C72.245 74.7122 72.781 75.2481 72.781 75.8945C72.781 76.5565 72.245 77.0925 71.5987 77.0925Z"
|
||||
fill="#4F46E5"
|
||||
/>
|
||||
<rect x="38.3522" y="21.5128" width="41.0256" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
<rect x="38.3522" y="133.65" width="54.7009" height="5.47009" rx="2.73504" fill="#A5B4FC" />
|
||||
<rect x="38.3522" y="29.7179" width="13.6752" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||
<circle cx="56.13" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
<circle cx="61.6001" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
<circle cx="67.0702" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||
</svg>
|
||||
<div>
|
||||
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">There’s no listing here</h2>
|
||||
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to <br />see listings</p>
|
||||
<div class="flex gap-3">
|
||||
<button (click)="clearAllFilters()" class="w-full px-3 py-2 rounded-full border border-gray-300 text-gray-900 text-xs font-semibold leading-4">Clear Filter</button>
|
||||
<button (click)="openFilterModal()" class="w-full px-3 py-2 bg-indigo-600 hover:bg-indigo-700 transition-all duration-500 rounded-full text-white text-xs font-semibold leading-4">Change Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if(pageCount>1){
|
||||
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
|
||||
|
||||
@@ -8,11 +8,13 @@ import { CommercialPropertyListing } from '../../../../../../bizmatch-server/src
|
||||
import { CommercialPropertyListingCriteria, LISTINGS_PER_PAGE, ResponseCommercialPropertyListingArray } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
|
||||
import { ModalService } from '../../../components/search-modal/modal.service';
|
||||
import { CriteriaChangeService } from '../../../services/criteria-change.service';
|
||||
import { ImageService } from '../../../services/image.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
import { SearchService } from '../../../services/search.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { getCriteriaProxy } from '../../../utils/utils';
|
||||
import { assignProperties, getCriteriaProxy, resetCommercialPropertyListingCriteria } from '../../../utils/utils';
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-commercial-property-listings',
|
||||
@@ -48,6 +50,8 @@ export class CommercialPropertyListingsComponent {
|
||||
private imageService: ImageService,
|
||||
private route: ActivatedRoute,
|
||||
private searchService: SearchService,
|
||||
private modalService: ModalService,
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
) {
|
||||
this.criteria = getCriteriaProxy('commercialPropertyListings', this) as CommercialPropertyListingCriteria;
|
||||
this.init();
|
||||
@@ -86,4 +90,28 @@ export class CommercialPropertyListingsComponent {
|
||||
getDaysListed(listing: CommercialPropertyListing) {
|
||||
return dayjs().diff(listing.created, 'day');
|
||||
}
|
||||
// New methods for filter actions
|
||||
clearAllFilters() {
|
||||
// Reset criteria to default values
|
||||
resetCommercialPropertyListingCriteria(this.criteria);
|
||||
|
||||
// Reset pagination
|
||||
this.criteria.page = 1;
|
||||
this.criteria.start = 0;
|
||||
|
||||
this.criteriaChangeService.notifyCriteriaChange();
|
||||
|
||||
// Search with cleared filters
|
||||
this.searchService.search(this.criteria);
|
||||
}
|
||||
|
||||
async openFilterModal() {
|
||||
// Open the search modal with current criteria
|
||||
const modalResult = await this.modalService.showModal(this.criteria);
|
||||
if (modalResult.accepted) {
|
||||
this.searchService.search(this.criteria);
|
||||
} else {
|
||||
this.criteria = assignProperties(this.criteria, modalResult.criteria);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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` });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container mx-auto p-4">
|
||||
@if (user){
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
|
||||
<form #accountForm="ngForm" class="space-y-4">
|
||||
<h2 class="text-2xl font-bold mb-4">Account Details</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -9,14 +9,14 @@
|
||||
<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>
|
||||
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative">
|
||||
@if(user?.hasCompanyLogo){
|
||||
<img src="{{ companyLogoUrl }}" alt="Company logo" class="max-w-full max-h-full" />
|
||||
<div class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 shadow-md hover:cursor-pointer" (click)="deleteConfirm('logo')">
|
||||
<div class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer" (click)="deleteConfirm('logo')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="w-20 h-20 w-full rounded-md flex items-center justify-center relative">
|
||||
@if(user?.hasProfile){
|
||||
<img src="{{ profileUrl }}" alt="Profile picture" class="max-w-full max-h-full" />
|
||||
<div class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 shadow-md hover:cursor-pointer" (click)="deleteConfirm('profile')">
|
||||
<div class="absolute top-[-0.5rem] right-[0rem] bg-white rounded-full p-1 drop-shadow-custom-bg hover:cursor-pointer" (click)="deleteConfirm('profile')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -71,22 +71,22 @@
|
||||
<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>
|
||||
} @if (isProfessional){
|
||||
}
|
||||
<app-validated-select [disabled]="true" 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>
|
||||
<select id="customerSubType" name="customerSubType" [(ngModel)]="user.customerSubType" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<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,31 @@ 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));
|
||||
|
||||
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 +232,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 : '';
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6">
|
||||
<h1 class="text-2xl font-semibold mb-6">Edit Listing</h1>
|
||||
@if(listing){
|
||||
<form #listingForm="ngForm" class="space-y-4">
|
||||
<div class="mb-4">
|
||||
<label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label>
|
||||
<ng-select
|
||||
[readonly]="mode === 'edit'"
|
||||
[readonly]="true"
|
||||
[items]="selectOptions?.listingCategories"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user