Compare commits
20 Commits
214327031c
...
primeng
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d5b7e3f39 | |||
| d488f90f48 | |||
| 044f8efa0f | |||
| 24a3d210f0 | |||
| 2465b8966b | |||
| e87222d3c1 | |||
| 902ab9caed | |||
| 44acbcd4d0 | |||
| b4cf17b8ea | |||
| 226d2ebc1e | |||
| 0473f74241 | |||
| c9d94e973a | |||
| f9d9c6ad9e | |||
| 5dc893da38 | |||
| c471629c6d | |||
| 13fb3cd4b8 | |||
| d6768b3da9 | |||
| 7fdc87fb0b | |||
| 0b7e33612a | |||
| 8fba3aa832 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,6 +48,9 @@ public
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.prod
|
||||||
|
.env.dev
|
||||||
|
.env.test
|
||||||
|
|
||||||
# temp directory
|
# temp directory
|
||||||
.temp
|
.temp
|
||||||
|
|||||||
3
bizmatch-server/.gitignore
vendored
3
bizmatch-server/.gitignore
vendored
@@ -55,4 +55,5 @@ pids
|
|||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
pictures
|
pictures
|
||||||
|
pictures_base
|
||||||
54
bizmatch-server/.vscode/launch.json
vendored
54
bizmatch-server/.vscode/launch.json
vendored
@@ -6,17 +6,15 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug Nest Framework",
|
"name": "Debug Nest Framework",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||||
"run",
|
|
||||||
"start:debug",
|
|
||||||
"--",
|
|
||||||
"--inspect-brk"
|
|
||||||
],
|
|
||||||
"autoAttachChildProcesses": true,
|
"autoAttachChildProcesses": true,
|
||||||
"restart": true,
|
"restart": true,
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"stopOnEntry": false,
|
"stopOnEntry": false,
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
"env": {
|
||||||
|
"HOST_NAME": "localhost"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
@@ -24,9 +22,7 @@
|
|||||||
"name": "Debug Current TS File",
|
"name": "Debug Current TS File",
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
|
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
|
||||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||||
"outFiles": [
|
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||||
"${workspaceFolder}/out/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"smartStep": true,
|
"smartStep": true,
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
@@ -35,31 +31,21 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "generateDefs",
|
"name": "generateDefs",
|
||||||
"skipFiles": [
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"<node_internals>/**"
|
|
||||||
],
|
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
||||||
"outFiles": [
|
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||||
"${workspaceFolder}/dist/src/drizzle/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"smartStep": true,
|
"smartStep": true
|
||||||
|
},
|
||||||
},
|
{
|
||||||
{
|
"type": "node",
|
||||||
"type": "node",
|
"request": "launch",
|
||||||
"request": "launch",
|
"name": "generateTypes",
|
||||||
"name": "generateTypes",
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"skipFiles": [
|
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
||||||
"<node_internals>/**"
|
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||||
],
|
"sourceMaps": true,
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
"smartStep": true
|
||||||
"outFiles": [
|
}
|
||||||
"${workspaceFolder}/dist/src/drizzle/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
|
||||||
"smartStep": true,
|
|
||||||
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "HOST_NAME=localhost nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -37,7 +37,9 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.30.8",
|
"drizzle-orm": "^0.30.8",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
"jwks-rsa": "^3.1.0",
|
||||||
"ky": "^1.2.0",
|
"ky": "^1.2.0",
|
||||||
"nest-winston": "^1.9.4",
|
"nest-winston": "^1.9.4",
|
||||||
"nodemailer": "^6.9.10",
|
"nodemailer": "^6.9.10",
|
||||||
@@ -109,4 +111,4 @@
|
|||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||||
import { AppService } from './app.service.js';
|
import { AppService } from './app.service.js';
|
||||||
|
import { AuthService } from './auth/auth.service.js';
|
||||||
|
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(
|
||||||
|
private readonly appService: AppService,
|
||||||
|
private authService: AuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
getHello(): string {
|
getHello(@Request() req): string {
|
||||||
return this.appService.getHello();
|
return req.user;
|
||||||
|
//return 'dfgdf';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import fs from 'fs-extra';
|
||||||
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
@@ -14,12 +17,37 @@ import { ListingsModule } from './listings/listings.module.js';
|
|||||||
import { MailModule } from './mail/mail.module.js';
|
import { MailModule } from './mail/mail.module.js';
|
||||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
||||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||||
|
|
||||||
import { UserModule } from './user/user.module.js';
|
import { UserModule } from './user/user.module.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
function loadEnvFiles() {
|
||||||
|
// Load the .env file
|
||||||
|
dotenv.config();
|
||||||
|
console.log('Loaded .env file');
|
||||||
|
|
||||||
|
// Determine which additional env file to load
|
||||||
|
let envFilePath = '';
|
||||||
|
const host = process.env.HOST_NAME || '';
|
||||||
|
|
||||||
|
if (host.includes('localhost')) {
|
||||||
|
envFilePath = '.env.local';
|
||||||
|
} else if (host.includes('dev.bizmatch.net')) {
|
||||||
|
envFilePath = '.env.dev';
|
||||||
|
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
|
||||||
|
envFilePath = '.env.prod';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the additional env file if it exists
|
||||||
|
if (fs.existsSync(envFilePath)) {
|
||||||
|
dotenv.config({ path: envFilePath });
|
||||||
|
console.log(`Loaded ${envFilePath} file`);
|
||||||
|
} else {
|
||||||
|
console.log(`No additional .env file found for HOST_NAME: ${host}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEnvFiles();
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
@@ -46,6 +74,7 @@ const __dirname = path.dirname(__filename);
|
|||||||
ListingsModule,
|
ListingsModule,
|
||||||
SelectOptionsModule,
|
SelectOptionsModule,
|
||||||
ImageModule,
|
ImageModule,
|
||||||
|
PassportModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, FileService],
|
providers: [AppService, FileService],
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { MailerModule } from '@nestjs-modules/mailer';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import path, { join } from 'path';
|
import path from 'path';
|
||||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { AuthService } from './auth.service.js';
|
import { JwtStrategy } from '../jwt.strategy.js';
|
||||||
import { AuthController } from './auth.controller.js';
|
import { AuthController } from './auth.controller.js';
|
||||||
|
import { AuthService } from './auth.service.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [PassportModule],
|
||||||
],
|
providers: [AuthService, JwtStrategy],
|
||||||
providers: [AuthService],
|
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports:[AuthService]
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
||||||
|
import fs from 'fs-extra';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import pkg from 'pg';
|
import pkg from 'pg';
|
||||||
import { rimraf } from 'rimraf';
|
import { rimraf } from 'rimraf';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { BusinessListing, CommercialPropertyListing, User, UserData } from 'src/models/db.model.js';
|
import { BusinessListing, CommercialPropertyListing, User, UserData } from 'src/models/db.model.js';
|
||||||
|
import { emailToDirName } from 'src/models/main.model.js';
|
||||||
import * as schema from './schema.js';
|
import * as schema from './schema.js';
|
||||||
const { Pool } = pkg;
|
const { Pool } = pkg;
|
||||||
|
|
||||||
@@ -32,6 +34,11 @@ const targetPathProfile = `./pictures/profile`;
|
|||||||
deleteFilesOfDir(targetPathProfile);
|
deleteFilesOfDir(targetPathProfile);
|
||||||
const targetPathLogo = `./pictures/logo`;
|
const targetPathLogo = `./pictures/logo`;
|
||||||
deleteFilesOfDir(targetPathLogo);
|
deleteFilesOfDir(targetPathLogo);
|
||||||
|
const targetPathProperty = `./pictures/property`;
|
||||||
|
deleteFilesOfDir(targetPathProperty);
|
||||||
|
fs.ensureDirSync(`./pictures/logo`);
|
||||||
|
fs.ensureDirSync(`./pictures/profile`);
|
||||||
|
fs.ensureDirSync(`./pictures/property`);
|
||||||
for (const userData of usersData) {
|
for (const userData of usersData) {
|
||||||
const user: User = { firstname: '', lastname: '', email: '' };
|
const user: User = { firstname: '', lastname: '', email: '' };
|
||||||
user.licensedIn = [];
|
user.licensedIn = [];
|
||||||
@@ -58,20 +65,21 @@ for (const userData of usersData) {
|
|||||||
user.gender = userData.gender;
|
user.gender = userData.gender;
|
||||||
user.created = new Date();
|
user.created = new Date();
|
||||||
user.updated = new Date();
|
user.updated = new Date();
|
||||||
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender });
|
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email });
|
||||||
generatedUserData.push(u[0].insertedId);
|
generatedUserData.push(u[0]);
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
if (u[0].gender === 'male') {
|
if (u[0].gender === 'male') {
|
||||||
male++;
|
male++;
|
||||||
const data = readFileSync(`./pictures/profile_base/Mann_${male}.jpg`);
|
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
||||||
await storeProfilePicture(data, u[0].insertedId);
|
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||||
} else {
|
} else {
|
||||||
female++;
|
female++;
|
||||||
const data = readFileSync(`./pictures/profile_base/Frau_${female}.jpg`);
|
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
|
||||||
await storeProfilePicture(data, u[0].insertedId);
|
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||||
}
|
}
|
||||||
const data = readFileSync(`./pictures/logos_base/${i}.jpg`);
|
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
|
||||||
await storeCompanyLogo(data, u[0].insertedId);
|
await storeCompanyLogo(data, emailToDirName(u[0].email));
|
||||||
}
|
}
|
||||||
//Business Listings
|
//Business Listings
|
||||||
filePath = `./data/businesses.json`;
|
filePath = `./data/businesses.json`;
|
||||||
@@ -82,7 +90,9 @@ for (const business of businessJsonData) {
|
|||||||
delete business.id;
|
delete business.id;
|
||||||
business.created = new Date(business.created);
|
business.created = new Date(business.created);
|
||||||
business.updated = new Date(business.created);
|
business.updated = new Date(business.created);
|
||||||
business.userId = getRandomItem(generatedUserData);
|
const user = getRandomItem(generatedUserData);
|
||||||
|
business.userId = user.insertedId;
|
||||||
|
business.imageName = emailToDirName(user.email);
|
||||||
await db.insert(schema.businesses).values(business);
|
await db.insert(schema.businesses).values(business);
|
||||||
}
|
}
|
||||||
//Corporate Listings
|
//Corporate Listings
|
||||||
@@ -92,14 +102,21 @@ const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // E
|
|||||||
for (const commercial of commercialJsonData) {
|
for (const commercial of commercialJsonData) {
|
||||||
const id = commercial.id;
|
const id = commercial.id;
|
||||||
delete commercial.id;
|
delete commercial.id;
|
||||||
|
const user = getRandomItem(generatedUserData);
|
||||||
commercial.imageOrder = getFilenames(id);
|
commercial.imageOrder = getFilenames(id);
|
||||||
commercial.imagePath = id;
|
commercial.imagePath = emailToDirName(user.email);
|
||||||
const insertionDate = getRandomDateWithinLastYear();
|
const insertionDate = getRandomDateWithinLastYear();
|
||||||
commercial.created = insertionDate;
|
commercial.created = insertionDate;
|
||||||
commercial.updated = insertionDate;
|
commercial.updated = insertionDate;
|
||||||
commercial.userId = getRandomItem(generatedUserData);
|
commercial.userId = user.insertedId;
|
||||||
await db.insert(schema.commercials).values(commercial);
|
commercial.draft = false;
|
||||||
|
const result = await db.insert(schema.commercials).values(commercial).returning();
|
||||||
|
//fs.ensureDirSync(`./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
|
||||||
|
try {
|
||||||
|
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`----- No pictures available for ${id} ------`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//End
|
//End
|
||||||
@@ -115,7 +132,7 @@ function getRandomItem<T>(arr: T[]): T {
|
|||||||
}
|
}
|
||||||
function getFilenames(id: string): string[] {
|
function getFilenames(id: string): string[] {
|
||||||
try {
|
try {
|
||||||
let filePath = `./pictures/property/${id}`;
|
let filePath = `./pictures_base/property/${id}`;
|
||||||
return readdirSync(filePath);
|
return readdirSync(filePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -141,14 +158,14 @@ async function storeProfilePicture(buffer: Buffer, userId: string) {
|
|||||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storeCompanyLogo(buffer: Buffer, userId: string) {
|
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(buffer)
|
const output = await sharp(buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
|
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
|
|||||||
"reasonForSale" varchar(255),
|
"reasonForSale" varchar(255),
|
||||||
"brokerLicencing" varchar(255),
|
"brokerLicencing" varchar(255),
|
||||||
"internals" text,
|
"internals" text,
|
||||||
|
"imagePath" varchar(200),
|
||||||
"created" timestamp,
|
"created" timestamp,
|
||||||
"updated" timestamp,
|
"updated" timestamp,
|
||||||
"visits" integer,
|
"visits" integer,
|
||||||
@@ -36,6 +37,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
|
|||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE IF NOT EXISTS "commercials" (
|
CREATE TABLE IF NOT EXISTS "commercials" (
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"serial_id" serial NOT NULL,
|
||||||
"userId" uuid,
|
"userId" uuid,
|
||||||
"type" integer,
|
"type" integer,
|
||||||
"title" varchar(255),
|
"title" varchar(255),
|
||||||
@@ -53,7 +55,7 @@ CREATE TABLE IF NOT EXISTS "commercials" (
|
|||||||
"website" varchar(255),
|
"website" varchar(255),
|
||||||
"phoneNumber" varchar(255),
|
"phoneNumber" varchar(255),
|
||||||
"imageOrder" varchar(200)[],
|
"imageOrder" varchar(200)[],
|
||||||
"imagePath" varchar(50),
|
"imagePath" varchar(200),
|
||||||
"created" timestamp,
|
"created" timestamp,
|
||||||
"updated" timestamp,
|
"updated" timestamp,
|
||||||
"visits" integer,
|
"visits" integer,
|
||||||
@@ -76,7 +78,9 @@ CREATE TABLE IF NOT EXISTS "users" (
|
|||||||
"hasProfile" boolean,
|
"hasProfile" boolean,
|
||||||
"hasCompanyLogo" boolean,
|
"hasCompanyLogo" boolean,
|
||||||
"licensedIn" jsonb,
|
"licensedIn" jsonb,
|
||||||
"gender" "gender"
|
"gender" "gender",
|
||||||
|
"created" timestamp,
|
||||||
|
"updated" timestamp
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE "users" ADD COLUMN "created" timestamp;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "updated" timestamp;
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE "customerType" AS ENUM('buyer', 'broker', 'professional');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ADD COLUMN "customerType" "customerType";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "98e2be90-3301-49a8-b323-78d9d8f79cb5",
|
"id": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "pg",
|
"dialect": "pg",
|
||||||
@@ -147,6 +147,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"imagePath": {
|
||||||
|
"name": "imagePath",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"name": "created",
|
"name": "created",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
@@ -202,6 +208,12 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "gen_random_uuid()"
|
"default": "gen_random_uuid()"
|
||||||
},
|
},
|
||||||
|
"serial_id": {
|
||||||
|
"name": "serial_id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
"userId": {
|
"userId": {
|
||||||
"name": "userId",
|
"name": "userId",
|
||||||
"type": "uuid",
|
"type": "uuid",
|
||||||
@@ -306,7 +318,7 @@
|
|||||||
},
|
},
|
||||||
"imagePath": {
|
"imagePath": {
|
||||||
"name": "imagePath",
|
"name": "imagePath",
|
||||||
"type": "varchar(50)",
|
"type": "varchar(200)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -454,6 +466,18 @@
|
|||||||
"type": "gender",
|
"type": "gender",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"name": "created",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated": {
|
||||||
|
"name": "updated",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "41802273-1335-433f-97cb-77774ddb3362",
|
"id": "0bc02618-4414-4e90-8c44-808737611da7",
|
||||||
"prevId": "98e2be90-3301-49a8-b323-78d9d8f79cb5",
|
"prevId": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "pg",
|
"dialect": "pg",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -147,6 +147,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"imagePath": {
|
||||||
|
"name": "imagePath",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"name": "created",
|
"name": "created",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
@@ -202,6 +208,12 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "gen_random_uuid()"
|
"default": "gen_random_uuid()"
|
||||||
},
|
},
|
||||||
|
"serial_id": {
|
||||||
|
"name": "serial_id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
"userId": {
|
"userId": {
|
||||||
"name": "userId",
|
"name": "userId",
|
||||||
"type": "uuid",
|
"type": "uuid",
|
||||||
@@ -306,7 +318,7 @@
|
|||||||
},
|
},
|
||||||
"imagePath": {
|
"imagePath": {
|
||||||
"name": "imagePath",
|
"name": "imagePath",
|
||||||
"type": "varchar(50)",
|
"type": "varchar(200)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -455,6 +467,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"customerType": {
|
||||||
|
"name": "customerType",
|
||||||
|
"type": "customerType",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"name": "created",
|
"name": "created",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
@@ -475,6 +493,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
|
"customerType": {
|
||||||
|
"name": "customerType",
|
||||||
|
"values": {
|
||||||
|
"buyer": "buyer",
|
||||||
|
"broker": "broker",
|
||||||
|
"professional": "professional"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gender": {
|
"gender": {
|
||||||
"name": "gender",
|
"name": "gender",
|
||||||
"values": {
|
"values": {
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1715627517508,
|
"when": 1716495198537,
|
||||||
"tag": "0000_open_hannibal_king",
|
"tag": "0000_burly_bruce_banner",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1715631674334,
|
"when": 1717085220861,
|
||||||
"tag": "0001_charming_thundra",
|
"tag": "0001_wet_mephistopheles",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
||||||
import { AreasServed, LicensedIn } from 'src/models/db.model';
|
import { AreasServed, LicensedIn } from 'src/models/db.model';
|
||||||
|
|
||||||
export const PG_CONNECTION = 'PG_CONNECTION';
|
export const PG_CONNECTION = 'PG_CONNECTION';
|
||||||
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
||||||
|
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'broker', 'professional']);
|
||||||
|
|
||||||
export const users = pgTable('users', {
|
export const users = pgTable('users', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
firstname: varchar('firstname', { length: 255 }).notNull(),
|
firstname: varchar('firstname', { length: 255 }).notNull(),
|
||||||
@@ -20,6 +22,7 @@ export const users = pgTable('users', {
|
|||||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||||
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||||
gender: genderEnum('gender'),
|
gender: genderEnum('gender'),
|
||||||
|
customerType: customerTypeEnum('customerType'),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
});
|
});
|
||||||
@@ -48,6 +51,7 @@ export const businesses = pgTable('businesses', {
|
|||||||
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
||||||
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
||||||
internals: text('internals'),
|
internals: text('internals'),
|
||||||
|
imageName: varchar('imagePath', { length: 200 }),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
visits: integer('visits'),
|
visits: integer('visits'),
|
||||||
@@ -56,6 +60,7 @@ export const businesses = pgTable('businesses', {
|
|||||||
|
|
||||||
export const commercials = pgTable('commercials', {
|
export const commercials = pgTable('commercials', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
serialId: serial('serial_id'),
|
||||||
userId: uuid('userId').references(() => users.id),
|
userId: uuid('userId').references(() => users.id),
|
||||||
type: integer('type'),
|
type: integer('type'),
|
||||||
title: varchar('title', { length: 255 }),
|
title: varchar('title', { length: 255 }),
|
||||||
@@ -73,7 +78,7 @@ export const commercials = pgTable('commercials', {
|
|||||||
website: varchar('website', { length: 255 }),
|
website: varchar('website', { length: 255 }),
|
||||||
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
||||||
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
||||||
imagePath: varchar('imagePath', { length: 50 }),
|
imagePath: varchar('imagePath', { length: 200 }),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
visits: integer('visits'),
|
visits: integer('visits'),
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export class FileService {
|
|||||||
fs.ensureDirSync(`./pictures/logo`);
|
fs.ensureDirSync(`./pictures/logo`);
|
||||||
fs.ensureDirSync(`./pictures/property`);
|
fs.ensureDirSync(`./pictures/property`);
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
|
// Subscriptions
|
||||||
|
// ############
|
||||||
private loadSubscriptions(): void {
|
private loadSubscriptions(): void {
|
||||||
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
||||||
const rawData = readFileSync(filePath, 'utf8');
|
const rawData = readFileSync(filePath, 'utf8');
|
||||||
@@ -28,36 +31,43 @@ export class FileService {
|
|||||||
getSubscriptions(): Subscription[] {
|
getSubscriptions(): Subscription[] {
|
||||||
return this.subscriptions;
|
return this.subscriptions;
|
||||||
}
|
}
|
||||||
async storeProfilePicture(file: Express.Multer.File, userId: string) {
|
// ############
|
||||||
|
// Profile
|
||||||
|
// ############
|
||||||
|
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(file.buffer)
|
const output = await sharp(file.buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
hasProfile(userId: string) {
|
hasProfile(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/profile/${userId}.avif`);
|
return fs.existsSync(`./pictures/profile/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
async storeCompanyLogo(file: Express.Multer.File, userId: string) {
|
// Logo
|
||||||
|
// ############
|
||||||
|
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(file.buffer)
|
const output = await sharp(file.buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
|
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||||
}
|
}
|
||||||
hasCompanyLogo(userId: string) {
|
hasCompanyLogo(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/logo/${userId}.avif`) ? true : false;
|
return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
async getPropertyImages(listingId: string): Promise<string[]> {
|
// Property
|
||||||
|
// ############
|
||||||
|
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
const directory = `./pictures/property/${listingId}`;
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
files.forEach(f => {
|
files.forEach(f => {
|
||||||
@@ -68,9 +78,9 @@ export class FileService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async hasPropertyImages(listingId: string): Promise<boolean> {
|
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
||||||
const result: ImageProperty[] = [];
|
const result: ImageProperty[] = [];
|
||||||
const directory = `./pictures/property/${listingId}`;
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
return files.length > 0;
|
return files.length > 0;
|
||||||
@@ -78,15 +88,18 @@ export class FileService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async storePropertyPicture(file: Express.Multer.File, listingId: string): Promise<string> {
|
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
||||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
||||||
const directory = `./pictures/property/${listingId}`;
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
fs.ensureDirSync(`${directory}`);
|
fs.ensureDirSync(`${directory}`);
|
||||||
const imageName = await this.getNextImageName(directory);
|
const imageName = await this.getNextImageName(directory);
|
||||||
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
||||||
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
|
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
|
||||||
return `${imageName}.avif`;
|
return `${imageName}.avif`;
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
|
// utils
|
||||||
|
// ############
|
||||||
async getNextImageName(directory) {
|
async getNextImageName(directory) {
|
||||||
try {
|
try {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
@@ -115,22 +128,6 @@ export class FileService {
|
|||||||
let timeTaken = Date.now() - start;
|
let timeTaken = Date.now() - start;
|
||||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
||||||
}
|
}
|
||||||
getProfileImagesForUsers(userids: string) {
|
|
||||||
const ids = userids.split(',');
|
|
||||||
let result = {};
|
|
||||||
for (const id of ids) {
|
|
||||||
result = { ...result, [id]: fs.existsSync(`./pictures/profile/${id}.avif`) };
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
getCompanyLogosForUsers(userids: string) {
|
|
||||||
const ids = userids.split(',');
|
|
||||||
let result = {};
|
|
||||||
for (const id of ids) {
|
|
||||||
result = { ...result, [id]: fs.existsSync(`./pictures/logo/${id}.avif`) };
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
deleteImage(path: string) {
|
deleteImage(path: string) {
|
||||||
fs.unlinkSync(path);
|
fs.unlinkSync(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Delete, Get, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
@@ -6,9 +6,6 @@ import { FileService } from '../file/file.service.js';
|
|||||||
import { ListingsService } from '../listings/listings.service.js';
|
import { ListingsService } from '../listings/listings.service.js';
|
||||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||||
|
|
||||||
import { commercials } from '../drizzle/schema.js';
|
|
||||||
import { CommercialPropertyListing } from '../models/db.model.js';
|
|
||||||
|
|
||||||
@Controller('image')
|
@Controller('image')
|
||||||
export class ImageController {
|
export class ImageController {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -17,58 +14,42 @@ export class ImageController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
private selectOptions: SelectOptionsService,
|
private selectOptions: SelectOptionsService,
|
||||||
) {}
|
) {}
|
||||||
|
// ############
|
||||||
@Post('uploadPropertyPicture/:imagePath')
|
// Property
|
||||||
|
// ############
|
||||||
|
@Post('uploadPropertyPicture/:imagePath/:serial')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string) {
|
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
|
||||||
const imagename = await this.fileService.storePropertyPicture(file, imagePath);
|
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
|
||||||
await this.listingService.addImage(imagePath, imagename);
|
await this.listingService.addImage(imagePath, serial, imagename);
|
||||||
}
|
}
|
||||||
|
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||||
@Post('uploadProfile/:id')
|
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
||||||
|
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
|
||||||
|
await this.listingService.deleteImage(imagePath, serial, imagename);
|
||||||
|
}
|
||||||
|
// ############
|
||||||
|
// Profile
|
||||||
|
// ############
|
||||||
|
@Post('uploadProfile/:email')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
|
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||||
await this.fileService.storeProfilePicture(file, id);
|
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||||
}
|
}
|
||||||
|
@Delete('profile/:email/')
|
||||||
@Post('uploadCompanyLogo/:id')
|
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||||
|
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||||
|
}
|
||||||
|
// ############
|
||||||
|
// Logo
|
||||||
|
// ############
|
||||||
|
@Post('uploadCompanyLogo/:email')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
|
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||||
await this.fileService.storeCompanyLogo(file, id);
|
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||||
}
|
}
|
||||||
|
@Delete('logo/:email/')
|
||||||
@Get(':id')
|
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||||
async getPropertyImagesById(@Param('id') id: string): Promise<any> {
|
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||||
const result = await this.listingService.findById(id, commercials);
|
|
||||||
const listing = result as CommercialPropertyListing;
|
|
||||||
if (listing.imageOrder) {
|
|
||||||
return listing.imageOrder;
|
|
||||||
} else {
|
|
||||||
const imageOrder = await this.fileService.getPropertyImages(id);
|
|
||||||
listing.imageOrder = imageOrder;
|
|
||||||
this.listingService.updateListing(listing.id, listing, commercials);
|
|
||||||
return imageOrder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Get('profileImages/:userids')
|
|
||||||
async getProfileImagesForUsers(@Param('userids') userids: string): Promise<any> {
|
|
||||||
return await this.fileService.getProfileImagesForUsers(userids);
|
|
||||||
}
|
|
||||||
@Get('companyLogos/:userids')
|
|
||||||
async getCompanyLogosForUsers(@Param('userids') userids: string): Promise<any> {
|
|
||||||
return await this.fileService.getCompanyLogosForUsers(userids);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('propertyPicture/:imagePath/:imagename')
|
|
||||||
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('imagename') imagename: string): Promise<any> {
|
|
||||||
this.fileService.deleteImage(`pictures/property/${imagePath}/${imagename}`);
|
|
||||||
}
|
|
||||||
@Delete('logo/:userid/')
|
|
||||||
async deleteLogoImagesById(@Param('userid') userid: string): Promise<any> {
|
|
||||||
this.fileService.deleteImage(`pictures/logo/${userid}.avif`);
|
|
||||||
}
|
|
||||||
@Delete('profile/:userid/')
|
|
||||||
async deleteProfileImagesById(@Param('userid') userid: string): Promise<any> {
|
|
||||||
this.fileService.deleteImage(`pictures/profile/${userid}.avif`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
bizmatch-server/src/jwt-auth/jwt-auth.guard.ts
Normal file
18
bizmatch-server/src/jwt-auth/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
bizmatch-server/src/jwt.strategy.ts
Normal file
45
bizmatch-server/src/jwt.strategy.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { passportJwtSecret } from 'jwks-rsa';
|
||||||
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import { JwtPayload, JwtUser } from './models/main.model';
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
|
) {
|
||||||
|
const realm = configService.get<string>('REALM');
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKeyProvider: passportJwtSecret({
|
||||||
|
cache: true,
|
||||||
|
rateLimit: true,
|
||||||
|
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, username: payload.preferred_username, roles: payload.realm_access?.roles };
|
||||||
|
this.logger.info(`JWT User: ${JSON.stringify(result)}`); // Debugging: JWT Payload anzeigen
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { businesses } from '../drizzle/schema.js';
|
import { businesses } from '../drizzle/schema.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||||
|
import { JwtUser, ListingCriteria } from '../models/main.model.js';
|
||||||
import { ListingsService } from './listings.service.js';
|
import { ListingsService } from './listings.service.js';
|
||||||
|
|
||||||
@Controller('listings/business')
|
@Controller('listings/business')
|
||||||
@@ -12,18 +13,22 @@ export class BusinessListingsController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findById(@Param('id') id: string): any {
|
findById(@Request() req, @Param('id') id: string): any {
|
||||||
return this.listingsService.findById(id, businesses);
|
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||||
}
|
|
||||||
@Get('user/:userid')
|
|
||||||
findByUserId(@Param('userid') userid: string): any {
|
|
||||||
return this.listingsService.findByUserId(userid, businesses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
|
@Get('user/:userid')
|
||||||
|
findByUserId(@Request() req, @Param('userid') userid: string): any {
|
||||||
|
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Post('search')
|
@Post('search')
|
||||||
find(@Body() criteria: ListingCriteria): any {
|
find(@Request() req, @Body() criteria: ListingCriteria): any {
|
||||||
return this.listingsService.findListingsByCriteria(criteria, businesses);
|
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@@ -34,7 +39,7 @@ export class BusinessListingsController {
|
|||||||
@Put()
|
@Put()
|
||||||
update(@Body() listing: any) {
|
update(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return this.listingsService.updateListing(listing.id, listing, businesses);
|
return this.listingsService.updateBusinessListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
deleteById(@Param('id') id: string) {
|
deleteById(@Param('id') id: string) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import { CommercialPropertyListing } from 'src/models/db.model.js';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { commercials } from '../drizzle/schema.js';
|
import { commercials } from '../drizzle/schema.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||||
|
import { JwtUser, ListingCriteria } from '../models/main.model.js';
|
||||||
import { ListingsService } from './listings.service.js';
|
import { ListingsService } from './listings.service.js';
|
||||||
|
|
||||||
@Controller('listings/commercialProperty')
|
@Controller('listings/commercialProperty')
|
||||||
@@ -14,17 +16,21 @@ export class CommercialPropertyListingsController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findById(@Param('id') id: string): any {
|
findById(@Request() req, @Param('id') id: string): any {
|
||||||
return this.listingsService.findById(id, commercials);
|
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@Get('user/:userid')
|
|
||||||
findByUserId(@Param('userid') userid: string): any {
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
return this.listingsService.findByUserId(userid, commercials);
|
@Get('user/:email')
|
||||||
|
findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||||
|
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Post('search')
|
@Post('search')
|
||||||
async find(@Body() criteria: ListingCriteria): Promise<any> {
|
async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
|
||||||
return await this.listingsService.findListingsByCriteria(criteria, commercials);
|
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@Get('states/all')
|
@Get('states/all')
|
||||||
getStates(): any {
|
getStates(): any {
|
||||||
@@ -38,16 +44,11 @@ export class CommercialPropertyListingsController {
|
|||||||
@Put()
|
@Put()
|
||||||
async update(@Body() listing: any) {
|
async update(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return await this.listingsService.updateListing(listing.id, listing, commercials);
|
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
@Delete(':id/:imagePath')
|
@Delete(':id/:imagePath')
|
||||||
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||||
this.listingsService.deleteListing(id, commercials);
|
this.listingsService.deleteListing(id, commercials);
|
||||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('imageOrder/:id')
|
|
||||||
async changeImageOrder(@Param('id') id: string, @Body() imageOrder: string[]) {
|
|
||||||
this.listingsService.updateImageOrder(id, imageOrder);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthModule } from '../auth/auth.module.js';
|
||||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { UserService } from '../user/user.service.js';
|
import { UserService } from '../user/user.service.js';
|
||||||
@@ -9,7 +10,7 @@ import { ListingsService } from './listings.service.js';
|
|||||||
import { UnknownListingsController } from './unknown-listings.controller.js';
|
import { UnknownListingsController } from './unknown-listings.controller.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DrizzleModule],
|
imports: [DrizzleModule, AuthModule],
|
||||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||||
providers: [ListingsService, FileService, UserService],
|
providers: [ListingsService, FileService, UserService],
|
||||||
exports: [ListingsService],
|
exports: [ListingsService],
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { and, eq, gte, ilike, lte, sql } from 'drizzle-orm';
|
import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
|
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
|
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
|
import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListingsService {
|
export class ListingsService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
|
private fileService: FileService,
|
||||||
) {}
|
) {}
|
||||||
private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials): any[] {
|
private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
if (criteria.type) {
|
if (criteria.type) {
|
||||||
conditions.push(eq(table.type, criteria.type));
|
conditions.push(eq(table.type, criteria.type));
|
||||||
@@ -39,46 +41,96 @@ export class ListingsService {
|
|||||||
// ##############################################################
|
// ##############################################################
|
||||||
// Listings general
|
// Listings general
|
||||||
// ##############################################################
|
// ##############################################################
|
||||||
async findListingsByCriteria(criteria: ListingCriteria, table: typeof businesses | typeof commercials): Promise<{ data: Record<string, any>[]; total: number }> {
|
|
||||||
|
async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
|
||||||
const start = criteria.start ? criteria.start : 0;
|
const start = criteria.start ? criteria.start : 0;
|
||||||
const length = criteria.length ? criteria.length : 12;
|
const length = criteria.length ? criteria.length : 12;
|
||||||
return await this.findListings(table, criteria, start, length);
|
const conditions = this.getConditions(criteria, commercials, user);
|
||||||
}
|
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||||
private async findListings(table: typeof businesses | typeof commercials, criteria: ListingCriteria, start = 0, length = 12): Promise<any> {
|
conditions.push(or(eq(commercials.draft, false), eq(commercials.imagePath, emailToDirName(user?.username))));
|
||||||
const conditions = this.getConditions(criteria, table);
|
}
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.conn
|
this.conn
|
||||||
.select()
|
.select()
|
||||||
.from(table)
|
.from(commercials)
|
||||||
.where(and(...conditions))
|
.where(and(...conditions))
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(length),
|
.limit(length),
|
||||||
this.conn
|
this.conn
|
||||||
.select({ count: sql`count(*)` })
|
.select({ count: sql`count(*)` })
|
||||||
.from(table)
|
.from(commercials)
|
||||||
.where(and(...conditions))
|
.where(and(...conditions))
|
||||||
.then(result => Number(result[0].count)),
|
.then(result => Number(result[0].count)),
|
||||||
]);
|
]);
|
||||||
return { total, data };
|
return { total, data };
|
||||||
}
|
}
|
||||||
async findById(id: string, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
async findBusinessListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
|
||||||
const result = await this.conn
|
const start = criteria.start ? criteria.start : 0;
|
||||||
.select()
|
const length = criteria.length ? criteria.length : 12;
|
||||||
.from(table)
|
const conditions = this.getConditions(criteria, businesses, user);
|
||||||
.where(sql`${table.id} = ${id}`);
|
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||||
return result[0] as BusinessListing | CommercialPropertyListing;
|
conditions.push(or(eq(businesses.draft, false), eq(businesses.imageName, emailToDirName(user?.username))));
|
||||||
|
}
|
||||||
|
const [data, total] = await Promise.all([
|
||||||
|
this.conn
|
||||||
|
.select()
|
||||||
|
.from(businesses)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.offset(start)
|
||||||
|
.limit(length),
|
||||||
|
this.conn
|
||||||
|
.select({ count: sql`count(*)` })
|
||||||
|
.from(businesses)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.then(result => Number(result[0].count)),
|
||||||
|
]);
|
||||||
|
return { total, data };
|
||||||
}
|
}
|
||||||
async findByImagePath(imagePath: string): Promise<CommercialPropertyListing> {
|
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||||
|
let result = await this.conn
|
||||||
|
.select()
|
||||||
|
.from(commercials)
|
||||||
|
.where(and(sql`${commercials.id} = ${id}`));
|
||||||
|
result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||||
|
return result[0] as CommercialPropertyListing;
|
||||||
|
}
|
||||||
|
async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||||
|
let result = await this.conn
|
||||||
|
.select()
|
||||||
|
.from(businesses)
|
||||||
|
.where(and(sql`${businesses.id} = ${id}`));
|
||||||
|
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||||
|
return result[0] as BusinessListing;
|
||||||
|
}
|
||||||
|
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||||
|
const conditions = [];
|
||||||
|
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
|
||||||
|
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||||
|
conditions.push(ne(commercials.draft, true));
|
||||||
|
}
|
||||||
|
return (await this.conn
|
||||||
|
.select()
|
||||||
|
.from(commercials)
|
||||||
|
.where(and(...conditions))) as CommercialPropertyListing[];
|
||||||
|
}
|
||||||
|
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
|
||||||
|
const conditions = [];
|
||||||
|
conditions.push(eq(businesses.imageName, emailToDirName(email)));
|
||||||
|
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||||
|
conditions.push(ne(businesses.draft, true));
|
||||||
|
}
|
||||||
|
return (await this.conn
|
||||||
|
.select()
|
||||||
|
.from(businesses)
|
||||||
|
.where(and(...conditions))) as CommercialPropertyListing[];
|
||||||
|
}
|
||||||
|
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
|
||||||
const result = await this.conn
|
const result = await this.conn
|
||||||
.select()
|
.select()
|
||||||
.from(commercials)
|
.from(commercials)
|
||||||
.where(sql`${commercials.imagePath} = ${imagePath}`);
|
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
|
||||||
return result[0] as CommercialPropertyListing;
|
return result[0] as CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
async findByUserId(userId: string, table: typeof businesses | typeof commercials): Promise<BusinessListing[] | CommercialPropertyListing[]> {
|
|
||||||
return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
data.created = new Date();
|
data.created = new Date();
|
||||||
data.updated = new Date();
|
data.updated = new Date();
|
||||||
@@ -86,13 +138,24 @@ export class ListingsService {
|
|||||||
return createdListing as BusinessListing | CommercialPropertyListing;
|
return createdListing as BusinessListing | CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateListing(id: string, data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
data.updated = new Date();
|
data.updated = new Date();
|
||||||
data.created = new Date(data.created);
|
data.created = new Date(data.created);
|
||||||
const [updateListing] = await this.conn.update(table).set(data).where(eq(table.id, id)).returning();
|
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
|
||||||
|
let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
|
||||||
|
if (difference.length > 0) {
|
||||||
|
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
|
||||||
|
data.imageOrder = imageOrder;
|
||||||
|
}
|
||||||
|
const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
|
||||||
|
return updateListing as BusinessListing | CommercialPropertyListing;
|
||||||
|
}
|
||||||
|
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
|
data.updated = new Date();
|
||||||
|
data.created = new Date(data.created);
|
||||||
|
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
|
||||||
return updateListing as BusinessListing | CommercialPropertyListing;
|
return updateListing as BusinessListing | CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
||||||
await this.conn.delete(table).where(eq(table.id, id));
|
await this.conn.delete(table).where(eq(table.id, id));
|
||||||
}
|
}
|
||||||
@@ -106,23 +169,17 @@ export class ListingsService {
|
|||||||
// ##############################################################
|
// ##############################################################
|
||||||
// Images for commercial Properties
|
// Images for commercial Properties
|
||||||
// ##############################################################
|
// ##############################################################
|
||||||
|
async deleteImage(imagePath: string, serial: string, name: string) {
|
||||||
async updateImageOrder(id: string, imageOrder: string[]) {
|
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||||
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
|
|
||||||
listing.imageOrder = imageOrder;
|
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
|
||||||
}
|
|
||||||
async deleteImage(id: string, name: string) {
|
|
||||||
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
|
|
||||||
const index = listing.imageOrder.findIndex(im => im === name);
|
const index = listing.imageOrder.findIndex(im => im === name);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
listing.imageOrder.splice(index, 1);
|
listing.imageOrder.splice(index, 1);
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async addImage(imagePath: string, imagename: string) {
|
async addImage(imagePath: string, serial: string, imagename: string) {
|
||||||
const listing = (await this.findByImagePath(imagePath)) as unknown as CommercialPropertyListing;
|
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||||
listing.imageOrder.push(imagename);
|
listing.imageOrder.push(imagename);
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Controller, Get, Inject, Param } from '@nestjs/common';
|
import { Controller, Inject } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { businesses, commercials } from '../drizzle/schema.js';
|
|
||||||
import { ListingsService } from './listings.service.js';
|
import { ListingsService } from './listings.service.js';
|
||||||
|
|
||||||
@Controller('listings/undefined')
|
@Controller('listings/undefined')
|
||||||
@@ -11,13 +10,13 @@ export class UnknownListingsController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get(':id')
|
// @Get(':id')
|
||||||
async findById(@Param('id') id: string): Promise<any> {
|
// async findById(@Param('id') id: string): Promise<any> {
|
||||||
const result = await this.listingsService.findById(id, businesses);
|
// const result = await this.listingsService.findById(id, businesses);
|
||||||
if (result) {
|
// if (result) {
|
||||||
return result;
|
// return result;
|
||||||
} else {
|
// } else {
|
||||||
return await this.listingsService.findById(id, commercials);
|
// return await this.listingsService.findById(id, commercials);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export class MailController {
|
|||||||
constructor(private mailService: MailService) {}
|
constructor(private mailService: MailService) {}
|
||||||
@Post()
|
@Post()
|
||||||
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||||
return this.mailService.sendInquiry(mailInfo);
|
if (mailInfo.listing) {
|
||||||
|
return this.mailService.sendInquiry(mailInfo);
|
||||||
|
} else {
|
||||||
|
return this.mailService.sendRequest(mailInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,23 @@ export class MailService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||||
|
if (isEmpty(mailInfo.sender.name)) {
|
||||||
|
return { fields: [{ fieldname: 'name', message: 'Required' }] };
|
||||||
|
}
|
||||||
|
await this.mailerService.sendMail({
|
||||||
|
to: 'support@bizmatch.net',
|
||||||
|
from: `"Bizmatch Support Team" <info@bizmatch.net>`,
|
||||||
|
subject: `Support Request from ${mailInfo.sender.name}`,
|
||||||
|
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||||
|
template: join(__dirname, '../..', 'mail/templates/request.hbs'),
|
||||||
|
context: {
|
||||||
|
// ✏️ filling curly brackets with content
|
||||||
|
request: mailInfo.sender.comments,
|
||||||
|
iname: mailInfo.sender.name,
|
||||||
|
phone: mailInfo.sender.phoneNumber,
|
||||||
|
email: mailInfo.sender.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Notification: New User Request</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.subheader {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
color: #1E90FF;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid #1E90FF;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.info:nth-child(even) {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">Notification: New request from a user of the Bizmatch Network</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Requester Information</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Contact Name:</span>
|
||||||
|
<span class="info-value">{{iname}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Contact Email:</span>
|
||||||
|
<span class="info-value">{{email}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Contact Phone:</span>
|
||||||
|
<span class="info-value">{{phone}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Question:</span>
|
||||||
|
<span class="info-value">{{request}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module.js';
|
import express from 'express';
|
||||||
import * as express from 'express';
|
import path from 'path';
|
||||||
import path, { join } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { AppModule } from './app.module.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
const server = express();
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.setGlobalPrefix('bizmatch');
|
app.setGlobalPrefix('bizmatch');
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: '*',
|
origin: '*',
|
||||||
|
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
allowedHeaders: 'Content-Type, Accept',
|
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||||
});
|
});
|
||||||
//origin: 'http://localhost:4200',
|
//origin: 'http://localhost:4200',
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface User {
|
|||||||
hasCompanyLogo?: boolean;
|
hasCompanyLogo?: boolean;
|
||||||
licensedIn?: LicensedIn[];
|
licensedIn?: LicensedIn[];
|
||||||
gender?: 'male' | 'female';
|
gender?: 'male' | 'female';
|
||||||
|
customerType?: 'buyer' | 'broker' | 'professional';
|
||||||
created?: Date;
|
created?: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ export interface UserData {
|
|||||||
hasCompanyLogo?: boolean;
|
hasCompanyLogo?: boolean;
|
||||||
licensedIn?: string[];
|
licensedIn?: string[];
|
||||||
gender?: 'male' | 'female';
|
gender?: 'male' | 'female';
|
||||||
|
customerType?: 'buyer' | 'broker' | 'professional';
|
||||||
created?: Date;
|
created?: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,7 @@ export interface BusinessListing {
|
|||||||
reasonForSale?: string;
|
reasonForSale?: string;
|
||||||
brokerLicencing?: string;
|
brokerLicencing?: string;
|
||||||
internals?: string;
|
internals?: string;
|
||||||
|
imageName?: string;
|
||||||
created?: Date;
|
created?: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
visits?: number;
|
visits?: number;
|
||||||
@@ -70,6 +73,7 @@ export interface BusinessListing {
|
|||||||
|
|
||||||
export interface CommercialPropertyListing {
|
export interface CommercialPropertyListing {
|
||||||
id: string;
|
id: string;
|
||||||
|
serialId?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
type?: number;
|
type?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ export interface KeycloakUser {
|
|||||||
notBefore?: number;
|
notBefore?: number;
|
||||||
access?: Access;
|
access?: Access;
|
||||||
}
|
}
|
||||||
|
export interface JwtUser {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
export interface Access {
|
export interface Access {
|
||||||
manageGroupMembership: boolean;
|
manageGroupMembership: boolean;
|
||||||
view: boolean;
|
view: boolean;
|
||||||
@@ -130,6 +135,14 @@ export interface JwtToken {
|
|||||||
email: string;
|
email: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
}
|
}
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
preferred_username: string;
|
||||||
|
realm_access?: {
|
||||||
|
roles?: string[];
|
||||||
|
};
|
||||||
|
[key: string]: any; // für andere optionale Felder im JWT-Payload
|
||||||
|
}
|
||||||
interface Resourceaccess {
|
interface Resourceaccess {
|
||||||
account: Realmaccess;
|
account: Realmaccess;
|
||||||
}
|
}
|
||||||
@@ -193,6 +206,9 @@ export function isEmpty(value: any): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
export function emailToDirName(email: string): string {
|
export function emailToDirName(email: string): string {
|
||||||
|
if (email === undefined || email === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
|
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
|
||||||
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestDurationMiddleware implements NestMiddleware {
|
export class RequestDurationMiddleware implements NestMiddleware {
|
||||||
@@ -8,8 +8,17 @@ export class RequestDurationMiddleware implements NestMiddleware {
|
|||||||
use(req: Request, res: Response, next: NextFunction) {
|
use(req: Request, res: Response, next: NextFunction) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
res.on('finish', () => {
|
res.on('finish', () => {
|
||||||
|
// const duration = Date.now() - start;
|
||||||
|
// this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
let logMessage = `${req.method} ${req.url} - ${duration}ms`;
|
||||||
|
|
||||||
|
if (req.method === 'POST' || req.method === 'PUT') {
|
||||||
|
const body = JSON.stringify(req.body);
|
||||||
|
logMessage += ` - Body: ${body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(logMessage);
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import { SelectOptionsService } from './select-options.service.js';
|
|||||||
|
|
||||||
@Controller('select-options')
|
@Controller('select-options')
|
||||||
export class SelectOptionsController {
|
export class SelectOptionsController {
|
||||||
constructor(private selectOptionsService:SelectOptionsService){}
|
constructor(private selectOptionsService: SelectOptionsService) {}
|
||||||
@Get()
|
@Get()
|
||||||
getSelectOption():any{
|
getSelectOption(): any {
|
||||||
return {
|
return {
|
||||||
typesOfBusiness:this.selectOptionsService.typesOfBusiness,
|
typesOfBusiness: this.selectOptionsService.typesOfBusiness,
|
||||||
prices:this.selectOptionsService.prices,
|
prices: this.selectOptionsService.prices,
|
||||||
listingCategories:this.selectOptionsService.listingCategories,
|
listingCategories: this.selectOptionsService.listingCategories,
|
||||||
categories:this.selectOptionsService.categories,
|
customerTypes: this.selectOptionsService.customerTypes,
|
||||||
locations:this.selectOptionsService.locations,
|
locations: this.selectOptionsService.locations,
|
||||||
typesOfCommercialProperty:this.selectOptionsService.typesOfCommercialProperty,
|
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,14 @@ export class SelectOptionsService {
|
|||||||
{ name: 'Business', value: 'business' },
|
{ name: 'Business', value: 'business' },
|
||||||
{ name: 'Commercial Property', value: 'commercialProperty' },
|
{ name: 'Commercial Property', value: 'commercialProperty' },
|
||||||
];
|
];
|
||||||
public categories: Array<KeyValueStyle> = [
|
public customerTypes: Array<KeyValue> = [
|
||||||
{ name: 'Broker', value: 'broker', icon: 'pi-image', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' },
|
{ name: 'Buyer', value: 'buyer' },
|
||||||
{ name: 'Professional', value: 'professional', icon: 'pi-globe', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
|
{ name: 'Broker', value: 'broker' },
|
||||||
|
{ name: 'Professional', value: 'professional' },
|
||||||
|
];
|
||||||
|
public gender: Array<KeyValue> = [
|
||||||
|
{ name: 'Male', value: 'male' },
|
||||||
|
{ name: 'Female', value: 'female' },
|
||||||
];
|
];
|
||||||
public imageTypes: ImageType[] = [
|
public imageTypes: ImageType[] = [
|
||||||
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
|
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as schema from '../drizzle/schema.js';
|
|||||||
import { PG_CONNECTION } from '../drizzle/schema.js';
|
import { PG_CONNECTION } from '../drizzle/schema.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { User } from '../models/db.model.js';
|
import { User } from '../models/db.model.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { ListingCriteria, emailToDirName } from '../models/main.model.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
@@ -33,8 +33,8 @@ export class UserService {
|
|||||||
.from(schema.users)
|
.from(schema.users)
|
||||||
.where(sql`email = ${email}`)) as User[];
|
.where(sql`email = ${email}`)) as User[];
|
||||||
const user = users[0];
|
const user = users[0];
|
||||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(user.id);
|
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||||
user.hasProfile = this.fileService.hasProfile(user.id);
|
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
async getUserById(id: string) {
|
async getUserById(id: string) {
|
||||||
@@ -43,8 +43,8 @@ export class UserService {
|
|||||||
.from(schema.users)
|
.from(schema.users)
|
||||||
.where(sql`id = ${id}`)) as User[];
|
.where(sql`id = ${id}`)) as User[];
|
||||||
const user = users[0];
|
const user = users[0];
|
||||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(id);
|
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||||
user.hasProfile = this.fileService.hasProfile(id);
|
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
async saveUser(user: any): Promise<User> {
|
async saveUser(user: any): Promise<User> {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"keycloak-angular": "^15.2.1",
|
||||||
"keycloak-js": "^24.0.4",
|
"keycloak-js": "^24.0.4",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"on-change": "^5.0.1",
|
"on-change": "^5.0.1",
|
||||||
@@ -66,4 +67,4 @@
|
|||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.3.3"
|
"typescript": "~5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, HostListener } from '@angular/core';
|
import { Component, HostListener } from '@angular/core';
|
||||||
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
import { ConfirmationService } from 'primeng/api';
|
import { ConfirmationService } from 'primeng/api';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
@@ -10,7 +11,6 @@ import { ListingCriteria } from '../../../bizmatch-server/src/models/main.model'
|
|||||||
import build from '../build';
|
import build from '../build';
|
||||||
import { FooterComponent } from './components/footer/footer.component';
|
import { FooterComponent } from './components/footer/footer.component';
|
||||||
import { HeaderComponent } from './components/header/header.component';
|
import { HeaderComponent } from './components/header/header.component';
|
||||||
import { KeycloakService } from './services/keycloak.service';
|
|
||||||
import { LoadingService } from './services/loading.service';
|
import { LoadingService } from './services/loading.service';
|
||||||
import { UserService } from './services/user.service';
|
import { UserService } from './services/user.service';
|
||||||
import { createDefaultListingCriteria } from './utils/utils';
|
import { createDefaultListingCriteria } from './utils/utils';
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScroll
|
|||||||
|
|
||||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
|
import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
|
import { customKeycloakAdapter } from '../keycloak';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
||||||
import { KeycloakInitializerService } from './services/keycloak-initializer.service';
|
import { KeycloakInitializerService } from './services/keycloak-initializer.service';
|
||||||
import { KeycloakService } from './services/keycloak.service';
|
|
||||||
import { SelectOptionsService } from './services/select-options.service';
|
import { SelectOptionsService } from './services/select-options.service';
|
||||||
|
import { createLogger } from './utils/utils';
|
||||||
// provideClientHydration()
|
// provideClientHydration()
|
||||||
|
const logger = createLogger('ApplicationConfig');
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
@@ -17,9 +20,10 @@ export const appConfig: ApplicationConfig = {
|
|||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
// useFactory: initializeKeycloak,
|
// useFactory: initializeKeycloak,
|
||||||
useFactory: (keycloakInitializer: KeycloakInitializerService) => async () => await keycloakInitializer.initialize(),
|
//useFactory: initializeKeycloak,
|
||||||
|
useFactory: initializeKeycloak3,
|
||||||
multi: true,
|
multi: true,
|
||||||
// deps: [KeycloakService],
|
//deps: [KeycloakService],
|
||||||
deps: [KeycloakInitializerService],
|
deps: [KeycloakInitializerService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -33,6 +37,11 @@ export const appConfig: ApplicationConfig = {
|
|||||||
useClass: LoadingInterceptor,
|
useClass: LoadingInterceptor,
|
||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: KeycloakBearerInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
provideRouter(
|
provideRouter(
|
||||||
routes,
|
routes,
|
||||||
withEnabledBlockingInitialNavigation(),
|
withEnabledBlockingInitialNavigation(),
|
||||||
@@ -49,9 +58,32 @@ function initServices(selectOptions: SelectOptionsService) {
|
|||||||
await selectOptions.init();
|
await selectOptions.init();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export function initializeKeycloak3(keycloak: KeycloakInitializerService) {
|
||||||
|
return () => keycloak.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeKeycloak2(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 initializeKeycloak(keycloak: KeycloakService) {
|
function initializeKeycloak(keycloak: KeycloakService) {
|
||||||
return async () => {
|
return async () => {
|
||||||
|
logger.info(`###>calling keycloakService init ...`);
|
||||||
const authenticated = await keycloak.init({
|
const authenticated = await keycloak.init({
|
||||||
config: {
|
config: {
|
||||||
url: environment.keycloak.url,
|
url: environment.keycloak.url,
|
||||||
@@ -62,7 +94,11 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
|||||||
onLoad: 'check-sso',
|
onLoad: 'check-sso',
|
||||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||||
},
|
},
|
||||||
|
bearerExcludedUrls: ['/assets'],
|
||||||
|
shouldUpdateToken(request) {
|
||||||
|
return !request.headers.get('token-update') === false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
console.log(`--->${authenticated}`);
|
logger.info(`+++>${authenticated}`);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { LogoutComponent } from './components/logout/logout.component';
|
import { LogoutComponent } from './components/logout/logout.component';
|
||||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||||
import { authGuard } from './guards/auth.guard';
|
|
||||||
|
import { AuthGuard } from './guards/auth.guard';
|
||||||
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
||||||
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
|
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
|
||||||
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
|
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
|
||||||
@@ -64,62 +65,62 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'account',
|
path: 'account',
|
||||||
component: AccountComponent,
|
component: AccountComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'account/:id',
|
path: 'account/:id',
|
||||||
component: AccountComponent,
|
component: AccountComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// Create, Update Listings
|
// Create, Update Listings
|
||||||
{
|
{
|
||||||
path: 'editBusinessListing/:id',
|
path: 'editBusinessListing/:id',
|
||||||
component: EditBusinessListingComponent,
|
component: EditBusinessListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'createBusinessListing',
|
path: 'createBusinessListing',
|
||||||
component: EditBusinessListingComponent,
|
component: EditBusinessListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'editCommercialPropertyListing/:id',
|
path: 'editCommercialPropertyListing/:id',
|
||||||
component: EditCommercialPropertyListingComponent,
|
component: EditCommercialPropertyListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'createCommercialPropertyListing',
|
path: 'createCommercialPropertyListing',
|
||||||
component: EditCommercialPropertyListingComponent,
|
component: EditCommercialPropertyListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// My Listings
|
// My Listings
|
||||||
{
|
{
|
||||||
path: 'myListings',
|
path: 'myListings',
|
||||||
component: MyListingComponent,
|
component: MyListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// My Favorites
|
// My Favorites
|
||||||
{
|
{
|
||||||
path: 'myFavorites',
|
path: 'myFavorites',
|
||||||
component: FavoritesComponent,
|
component: FavoritesComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// EMAil Us
|
// EMAil Us
|
||||||
{
|
{
|
||||||
path: 'emailUs',
|
path: 'emailUs',
|
||||||
component: EmailUsComponent,
|
component: EmailUsComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// Logout
|
// Logout
|
||||||
{
|
{
|
||||||
path: 'logout',
|
path: 'logout',
|
||||||
component: LogoutComponent,
|
component: LogoutComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// Pricing
|
// Pricing
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<div class="col-12 md:col-3 text-500">
|
<div class="col-12 md:col-3 text-500">
|
||||||
<div class="text-black font-bold line-height-3 mb-3">Actions</div>
|
<div class="text-black font-bold line-height-3 mb-3">Actions</div>
|
||||||
<a *ngIf="!keycloakService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
|
<a *ngIf="!keycloakService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
|
||||||
|
<a *ngIf="!keycloakService.isLoggedIn()" (click)="register()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Register</a>
|
||||||
<a *ngIf="keycloakService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
|
<a *ngIf="keycloakService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
|
||||||
<a *ngIf="keycloakService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="keycloakService.logout()">Log Out</a>
|
<a *ngIf="keycloakService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="keycloakService.logout()">Log Out</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { SidebarModule } from 'primeng/sidebar';
|
import { SidebarModule } from 'primeng/sidebar';
|
||||||
import { KeycloakService } from '../../services/keycloak.service';
|
|
||||||
import { SharedModule } from '../../shared/shared/shared.module';
|
import { SharedModule } from '../../shared/shared/shared.module';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'footer',
|
selector: 'footer',
|
||||||
@@ -18,4 +18,7 @@ export class FooterComponent {
|
|||||||
redirectUri: window.location.href,
|
redirectUri: window.location.href,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
register() {
|
||||||
|
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { MenuItem } from 'primeng/api';
|
import { MenuItem } from 'primeng/api';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { MenubarModule } from 'primeng/menubar';
|
import { MenubarModule } from 'primeng/menubar';
|
||||||
@@ -10,7 +11,6 @@ import { TabMenuModule } from 'primeng/tabmenu';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { KeycloakService } from '../../services/keycloak.service';
|
|
||||||
import { map2User } from '../../utils/utils';
|
import { map2User } from '../../utils/utils';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header',
|
selector: 'header',
|
||||||
@@ -91,14 +91,17 @@ export class HeaderComponent {
|
|||||||
{
|
{
|
||||||
label: 'Businesses for Sale',
|
label: 'Businesses for Sale',
|
||||||
routerLink: '/businessListings',
|
routerLink: '/businessListings',
|
||||||
|
state: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Commercial Property',
|
label: 'Commercial Property',
|
||||||
routerLink: '/commercialPropertyListings',
|
routerLink: '/commercialPropertyListings',
|
||||||
|
state: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Professionals/Brokers Directory',
|
label: 'Professionals/Brokers Directory',
|
||||||
routerLink: '/brokerListings',
|
routerLink: '/brokerListings',
|
||||||
|
state: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.loginItems = [
|
this.loginItems = [
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="cropperConfig"></angular-cropper>
|
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="cropperConfig"></angular-cropper>
|
||||||
<div class="flex justify-content-between mt-3">
|
<div class="flex justify-content-between mt-3">
|
||||||
@if(ratioVariable){
|
@if(ratioVariable){
|
||||||
<div>
|
<div>
|
||||||
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)"
|
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)" optionLabel="label" optionValue="value" class="small"></p-selectButton>
|
||||||
optionLabel="label" optionValue="value"></p-selectButton>
|
</div>
|
||||||
</div>
|
} @else {
|
||||||
} @else {
|
<div></div>
|
||||||
<div></div>
|
}
|
||||||
}
|
<div class="flex justify-content-between">
|
||||||
<div>
|
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true" size="small" class="mr-2"></p-button>
|
||||||
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button>
|
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true" size="small"></p-button>
|
||||||
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
::ng-deep p-selectbutton.small .p-button {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.65625rem 1.09375rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,67 +1,52 @@
|
|||||||
import { Component, ViewChild } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
|
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
|
||||||
import { LoadingService } from '../../services/loading.service';
|
|
||||||
import { ImageService } from '../../services/image.service';
|
|
||||||
import { HttpEventType } from '@angular/common/http';
|
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
|
||||||
import { SharedModule } from '../../shared/shared/shared.module';
|
|
||||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
import { SelectButtonModule } from 'primeng/selectbutton';
|
||||||
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
|
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
export const stateOptions:KeyValueRatio[]=[
|
import { ImageService } from '../../services/image.service';
|
||||||
{label:'16/9',value:16/9},
|
import { LoadingService } from '../../services/loading.service';
|
||||||
{label:'1/1',value:1},
|
import { SharedModule } from '../../shared/shared/shared.module';
|
||||||
{label:'2/3',value:2/3},
|
export const stateOptions: KeyValueRatio[] = [
|
||||||
]
|
{ label: '16/9', value: 16 / 9 },
|
||||||
|
{ label: '1/1', value: 1 },
|
||||||
|
{ label: 'Free', value: NaN },
|
||||||
|
];
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-image-cropper',
|
selector: 'app-image-cropper',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule,FileUploadModule,AngularCropperjsModule,SelectButtonModule],
|
imports: [SharedModule, FileUploadModule, AngularCropperjsModule, SelectButtonModule],
|
||||||
templateUrl: './image-cropper.component.html',
|
templateUrl: './image-cropper.component.html',
|
||||||
styleUrl: './image-cropper.component.scss'
|
styleUrl: './image-cropper.component.scss',
|
||||||
})
|
})
|
||||||
export class ImageCropperComponent {
|
export class ImageCropperComponent {
|
||||||
@ViewChild(CropperComponent) public angularCropper: CropperComponent;
|
@ViewChild(CropperComponent) public angularCropper: CropperComponent;
|
||||||
imageUrl:string; //wird im Template verwendet
|
imageUrl: string; //wird im Template verwendet
|
||||||
fileUpload:FileUpload
|
fileUpload: FileUpload;
|
||||||
value:number = stateOptions[0].value;
|
value: number = stateOptions[0].value;
|
||||||
cropperConfig={aspectRatio: this.value}
|
cropperConfig = { aspectRatio: this.value };
|
||||||
ratioVariable:boolean
|
ratioVariable: boolean;
|
||||||
stateOptions=stateOptions
|
stateOptions = stateOptions;
|
||||||
constructor(
|
constructor(private loadingService: LoadingService, private imageUploadService: ImageService, public config: DynamicDialogConfig, public ref: DynamicDialogRef) {}
|
||||||
private loadingService:LoadingService,
|
|
||||||
private imageUploadService: ImageService,
|
|
||||||
public config: DynamicDialogConfig,
|
|
||||||
public ref: DynamicDialogRef
|
|
||||||
){}
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.config.data) {
|
if (this.config.data) {
|
||||||
this.imageUrl = this.config.data.imageUrl;
|
this.imageUrl = this.config.data.imageUrl;
|
||||||
this.fileUpload = this.config.data.fileUpload;
|
this.fileUpload = this.config.data.fileUpload;
|
||||||
this.cropperConfig = this.config.data.config ? this.config.data.config: this.cropperConfig;
|
this.cropperConfig = this.config.data.config ? this.config.data.config : this.cropperConfig;
|
||||||
this.ratioVariable = this.config.data.ratioVariable;
|
this.ratioVariable = this.config.data.ratioVariable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendImage(){
|
sendImage() {
|
||||||
// setTimeout(()=>{
|
this.fileUpload.clear();
|
||||||
// this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => {
|
|
||||||
// this.ref.close(blob);
|
|
||||||
// this.fileUpload.clear()
|
|
||||||
// }, 'image/jpg');
|
|
||||||
// },0)
|
|
||||||
|
|
||||||
this.fileUpload.clear()
|
|
||||||
this.ref.close(this.angularCropper.cropper);
|
this.ref.close(this.angularCropper.cropper);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload(){
|
cancelUpload() {
|
||||||
this.fileUpload.clear();
|
this.fileUpload.clear();
|
||||||
this.ref.close();
|
this.ref.close();
|
||||||
}
|
}
|
||||||
changeAspectRation(ratio:number){
|
changeAspectRation(ratio: number) {
|
||||||
this.cropperConfig={aspectRatio: ratio}
|
this.cropperConfig = { aspectRatio: ratio };
|
||||||
this.angularCropper.cropper.setAspectRatio(ratio);
|
this.angularCropper.cropper.setAspectRatio(ratio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { KeycloakService } from '../../services/keycloak.service';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'logout',
|
selector: 'logout',
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import { inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CanMatchFn, Router, UrlTree } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||||
|
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
|
||||||
// Services
|
|
||||||
import { KeycloakInitializerService } from '../services/keycloak-initializer.service';
|
import { KeycloakInitializerService } from '../services/keycloak-initializer.service';
|
||||||
import { KeycloakService } from '../services/keycloak.service';
|
|
||||||
import { createLogger } from '../utils/utils';
|
import { createLogger } from '../utils/utils';
|
||||||
const logger = createLogger('authGuard');
|
const logger = createLogger('AuthGuard');
|
||||||
export const authGuard: CanMatchFn = async (route, segments): Promise<boolean | UrlTree> => {
|
@Injectable({
|
||||||
const router = inject(Router);
|
providedIn: 'root',
|
||||||
const keycloakService = inject(KeycloakService);
|
})
|
||||||
const keycloakInitializer = inject(KeycloakInitializerService);
|
export class AuthGuard extends KeycloakAuthGuard {
|
||||||
if (!keycloakInitializer.isInitialized()) {
|
constructor(protected override readonly router: Router, protected readonly keycloak: KeycloakService, private keycloakInitializer: KeycloakInitializerService) {
|
||||||
await keycloakInitializer.initialize();
|
super(router, keycloak);
|
||||||
}
|
|
||||||
logger.info('###-> calling isLoggedIn()');
|
|
||||||
const authenticated = keycloakService.isLoggedIn();
|
|
||||||
if (!authenticated) {
|
|
||||||
console.log(window.location.origin);
|
|
||||||
console.log(window.location.href);
|
|
||||||
keycloakService.login({
|
|
||||||
redirectUri: `${window.location.origin}${segments['url']}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the user Keycloak roles and the required from the route
|
async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree> {
|
||||||
const roles: string[] = keycloakService.getUserRoles(true);
|
logger.info(`--->AuthGuard`);
|
||||||
const requiredRoles = route.data?.['roles'];
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow the user to proceed if no additional roles are required to access the route
|
// Get the roles required from the route.
|
||||||
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
|
const requiredRoles = route.data['roles'];
|
||||||
return true;
|
|
||||||
|
// Allow the user to proceed if no additional roles are required to access the route.
|
||||||
|
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the user to proceed if all the required roles are present.
|
||||||
|
return requiredRoles.every(role => this.roles.includes(role));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const authorized = requiredRoles.every(role => roles.includes(role));
|
|
||||||
|
|
||||||
if (authorized) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.createUrlTree(['/home']);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
import {
|
|
||||||
HttpInterceptor,
|
|
||||||
HttpRequest,
|
|
||||||
HttpHandler,
|
|
||||||
HttpEvent,
|
|
||||||
HttpInterceptorFn,
|
|
||||||
HttpHandlerFn,
|
|
||||||
} from '@angular/common/http';
|
|
||||||
|
|
||||||
import { Observable, combineLatest, from, of } from 'rxjs';
|
|
||||||
import { mergeMap } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { KeycloakService } from '../services/keycloak.service';
|
|
||||||
import { ExcludedUrlRegex } from '../models/keycloak-options';
|
|
||||||
|
|
||||||
export const keycloakBearerInterceptor: HttpInterceptorFn = (req, next) => {
|
|
||||||
//return next(req);
|
|
||||||
const keycloak = inject(KeycloakService);
|
|
||||||
const { enableBearerInterceptor, excludedUrls } = keycloak;
|
|
||||||
if (!enableBearerInterceptor) {
|
|
||||||
return next(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
const shallPass: boolean =
|
|
||||||
!keycloak.shouldAddToken(req) ||
|
|
||||||
excludedUrls.findIndex((item) => isUrlExcluded(req, item)) > -1;
|
|
||||||
if (shallPass) {
|
|
||||||
return next(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
return combineLatest([
|
|
||||||
from(conditionallyUpdateToken(req)),
|
|
||||||
of(keycloak.isLoggedIn()),
|
|
||||||
]).pipe(
|
|
||||||
mergeMap(([_, isLoggedIn]) =>
|
|
||||||
isLoggedIn ? handleRequestWithTokenHeader(req, next) : next(req)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function isUrlExcluded(
|
|
||||||
{ method, url }: HttpRequest<unknown>,
|
|
||||||
{ urlPattern, httpMethods }: ExcludedUrlRegex
|
|
||||||
): boolean {
|
|
||||||
const httpTest =
|
|
||||||
httpMethods.length === 0 ||
|
|
||||||
httpMethods.join().indexOf(method.toUpperCase()) > -1;
|
|
||||||
|
|
||||||
const urlTest = urlPattern.test(url);
|
|
||||||
|
|
||||||
return httpTest && urlTest;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRequestWithTokenHeader(
|
|
||||||
req: HttpRequest<unknown>,
|
|
||||||
next: HttpHandlerFn
|
|
||||||
): Observable<HttpEvent<unknown>> {
|
|
||||||
return this.keycloak.addTokenToHeader(req.headers).pipe(
|
|
||||||
mergeMap((headersWithBearer:string) => {
|
|
||||||
const kcReq = req.clone({
|
|
||||||
headers: req.headers.set('Authorization', headersWithBearer)
|
|
||||||
});//req.clone({ headers: headersWithBearer });
|
|
||||||
return next(kcReq);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function conditionallyUpdateToken(
|
|
||||||
req: HttpRequest<unknown>
|
|
||||||
): Promise<boolean> {
|
|
||||||
if (this.keycloak.shouldUpdateToken(req)) {
|
|
||||||
return await this.keycloak.updateToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Mauricio Gemelli Vigolo and contributors.
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by a MIT-style license that can be
|
|
||||||
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keycloak event types, as described at the keycloak-js documentation:
|
|
||||||
* https://www.keycloak.org/docs/latest/securing_apps/index.html#callback-events
|
|
||||||
*/
|
|
||||||
export enum KeycloakEventType {
|
|
||||||
/**
|
|
||||||
* Called if there was an error during authentication.
|
|
||||||
*/
|
|
||||||
OnAuthError,
|
|
||||||
/**
|
|
||||||
* Called if the user is logged out
|
|
||||||
* (will only be called if the session status iframe is enabled, or in Cordova mode).
|
|
||||||
*/
|
|
||||||
OnAuthLogout,
|
|
||||||
/**
|
|
||||||
* Called if there was an error while trying to refresh the token.
|
|
||||||
*/
|
|
||||||
OnAuthRefreshError,
|
|
||||||
/**
|
|
||||||
* Called when the token is refreshed.
|
|
||||||
*/
|
|
||||||
OnAuthRefreshSuccess,
|
|
||||||
/**
|
|
||||||
* Called when a user is successfully authenticated.
|
|
||||||
*/
|
|
||||||
OnAuthSuccess,
|
|
||||||
/**
|
|
||||||
* Called when the adapter is initialized.
|
|
||||||
*/
|
|
||||||
OnReady,
|
|
||||||
/**
|
|
||||||
* Called when the access token is expired. If a refresh token is available the token
|
|
||||||
* can be refreshed with updateToken, or in cases where it is not (that is, with implicit flow)
|
|
||||||
* you can redirect to login screen to obtain a new access token.
|
|
||||||
*/
|
|
||||||
OnTokenExpired,
|
|
||||||
/**
|
|
||||||
* Called when a AIA has been requested by the application.
|
|
||||||
*/
|
|
||||||
OnActionUpdate,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Structure of an event triggered by Keycloak, contains it's type
|
|
||||||
* and arguments (if any).
|
|
||||||
*/
|
|
||||||
export interface KeycloakEvent {
|
|
||||||
/**
|
|
||||||
* Event type as described at {@link KeycloakEventType}.
|
|
||||||
*/
|
|
||||||
type: KeycloakEventType;
|
|
||||||
/**
|
|
||||||
* Arguments from the keycloak-js event function.
|
|
||||||
*/
|
|
||||||
args?: unknown;
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Mauricio Gemelli Vigolo and contributors.
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by a MIT-style license that can be
|
|
||||||
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { HttpRequest } from '@angular/common/http';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP Methods
|
|
||||||
*/
|
|
||||||
export type HttpMethods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ExcludedUrl type may be used to specify the url and the HTTP method that
|
|
||||||
* should not be intercepted by the KeycloakBearerInterceptor.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* const excludedUrl: ExcludedUrl[] = [
|
|
||||||
* {
|
|
||||||
* url: 'reports/public'
|
|
||||||
* httpMethods: ['GET']
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
*
|
|
||||||
* In the example above for URL reports/public and HTTP Method GET the
|
|
||||||
* bearer will not be automatically added.
|
|
||||||
*
|
|
||||||
* If the url is informed but httpMethod is undefined, then the bearer
|
|
||||||
* will not be added for all HTTP Methods.
|
|
||||||
*/
|
|
||||||
export interface ExcludedUrl {
|
|
||||||
url: string;
|
|
||||||
httpMethods?: HttpMethods[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Similar to ExcludedUrl, contains the HTTP methods and a regex to
|
|
||||||
* include the url patterns.
|
|
||||||
* This interface is used internally by the KeycloakService.
|
|
||||||
*/
|
|
||||||
export interface ExcludedUrlRegex {
|
|
||||||
urlPattern: RegExp;
|
|
||||||
httpMethods?: HttpMethods[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* keycloak-angular initialization options.
|
|
||||||
*/
|
|
||||||
export interface KeycloakOptions {
|
|
||||||
/**
|
|
||||||
* Configs to init the keycloak-js library. If undefined, will look for a keycloak.json file
|
|
||||||
* at root of the project.
|
|
||||||
* If not undefined, can be a string meaning the url to the keycloak.json file or an object
|
|
||||||
* of {@link Keycloak.KeycloakConfig}. Use this configuration if you want to specify the keycloak server,
|
|
||||||
* realm, clientId. This is usefull if you have different configurations for production, stage
|
|
||||||
* and development environments. Hint: Make use of Angular environment configuration.
|
|
||||||
*/
|
|
||||||
config?: string | Keycloak.KeycloakConfig;
|
|
||||||
/**
|
|
||||||
* Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself.
|
|
||||||
*/
|
|
||||||
initOptions?: Keycloak.KeycloakInitOptions;
|
|
||||||
/**
|
|
||||||
* By default all requests made by Angular HttpClient will be intercepted in order to
|
|
||||||
* add the bearer in the Authorization Http Header. However, if this is a not desired
|
|
||||||
* feature, the enableBearerInterceptor must be false.
|
|
||||||
*
|
|
||||||
* Briefly, if enableBearerInterceptor === false, the bearer will not be added
|
|
||||||
* to the authorization header.
|
|
||||||
*
|
|
||||||
* The default value is true.
|
|
||||||
*/
|
|
||||||
enableBearerInterceptor?: boolean;
|
|
||||||
/**
|
|
||||||
* Forces the execution of loadUserProfile after the keycloak initialization considering that the
|
|
||||||
* user logged in.
|
|
||||||
* This option is recommended if is desirable to have the user details at the beginning,
|
|
||||||
* so after the login, the loadUserProfile function will be called and its value cached.
|
|
||||||
*
|
|
||||||
* The default value is true.
|
|
||||||
*/
|
|
||||||
loadUserProfileAtStartUp?: boolean;
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* String Array to exclude the urls that should not have the Authorization Header automatically
|
|
||||||
* added. This library makes use of Angular Http Interceptor, to automatically add the Bearer
|
|
||||||
* token to the request.
|
|
||||||
*/
|
|
||||||
bearerExcludedUrls?: (string | ExcludedUrl)[];
|
|
||||||
/**
|
|
||||||
* This value will be used as the Authorization Http Header name. The default value is
|
|
||||||
* **Authorization**. If the backend expects requests to have a token in a different header, you
|
|
||||||
* should change this value, i.e: **JWT-Authorization**. This will result in a Http Header
|
|
||||||
* Authorization as "JWT-Authorization: bearer <token>".
|
|
||||||
*/
|
|
||||||
authorizationHeaderName?: string;
|
|
||||||
/**
|
|
||||||
* This value will be included in the Authorization Http Header param. The default value is
|
|
||||||
* **Bearer**, which will result in a Http Header Authorization as "Authorization: Bearer <token>".
|
|
||||||
*
|
|
||||||
* If any other value is needed by the backend in the authorization header, you should change this
|
|
||||||
* value.
|
|
||||||
*
|
|
||||||
* Warning: this value must be in compliance with the keycloak server instance and the adapter.
|
|
||||||
*/
|
|
||||||
bearerPrefix?: string;
|
|
||||||
/**
|
|
||||||
* This value will be used to determine whether or not the token needs to be updated. If the token
|
|
||||||
* will expire is fewer seconds than the updateMinValidity value, then it will be updated.
|
|
||||||
*
|
|
||||||
* The default value is 20.
|
|
||||||
*/
|
|
||||||
updateMinValidity?: number;
|
|
||||||
/**
|
|
||||||
* A function that will tell the KeycloakBearerInterceptor whether to add the token to the request
|
|
||||||
* or to leave the request as it is. If the returned value is `true`, the request will have the token
|
|
||||||
* present on it. If it is `false`, the token will be left off the request.
|
|
||||||
*
|
|
||||||
* The default is a function that always returns `true`.
|
|
||||||
*/
|
|
||||||
shouldAddToken?: (request: HttpRequest<unknown>) => boolean;
|
|
||||||
/**
|
|
||||||
* A function that will tell the KeycloakBearerInterceptor if the token should be considered for
|
|
||||||
* updating as a part of the request being made. If the returned value is `true`, the request will
|
|
||||||
* check the token's expiry time and if it is less than the number of seconds configured by
|
|
||||||
* updateMinValidity then it will be updated before the request is made. If the returned value is
|
|
||||||
* false, the token will not be updated.
|
|
||||||
*
|
|
||||||
* The default is a function that always returns `true`.
|
|
||||||
*/
|
|
||||||
shouldUpdateToken?: (request: HttpRequest<unknown>) => boolean;
|
|
||||||
}
|
|
||||||
@@ -14,50 +14,62 @@
|
|||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium flex">Description</div>
|
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
|
||||||
<div class="text-900 w-full md:w-10 line-height-3 flex flex-column" [innerHTML]="description"></div>
|
<div class="text-900 w-full md:w-9 line-height-3 flex flex-column" [innerHTML]="description"></div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Category</div>
|
<div class="text-500 w-full md:w-3 font-medium">Category</div>
|
||||||
<div class="text-900 w-full md:w-10">
|
<div class="text-900 w-full md:w-9">
|
||||||
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
|
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
|
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.city }}, {{ selectOptions.getState(listing.state) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Asking Price</div>
|
<div class="text-500 w-full md:w-3 font-medium">Asking Price</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.price | currency }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Real Estate Included</div>
|
<div class="text-500 w-full md:w-3 font-medium">Sales revenue</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.realEstateIncluded ? 'Yes' : 'No' }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.salesRevenue | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Sales revenue</div>
|
<div class="text-500 w-full md:w-3 font-medium">Cash flow</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.salesRevenue | currency }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.cashFlow | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Cash flow</div>
|
<div class="text-500 w-full md:w-3 font-medium">Type of Real Estate</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.cashFlow | currency }}</div>
|
<div class="text-900 w-full md:w-9">
|
||||||
|
@if (listing.realEstateIncluded){
|
||||||
|
<p-chip label="Real Estate Included"></p-chip>
|
||||||
|
} @if (listing.leasedLocation){
|
||||||
|
<p-chip label="Leased Location"></p-chip>
|
||||||
|
} @if (listing.franchiseResale){
|
||||||
|
<p-chip label="Franchise Re-Sale"></p-chip>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Employees</div>
|
<div class="text-500 w-full md:w-3 font-medium">Employees</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.employees }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.employees }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Support & Training</div>
|
<div class="text-500 w-full md:w-3 font-medium">Established since</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.supportAndTraining }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.established }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Reason for Sale</div>
|
<div class="text-500 w-full md:w-3 font-medium">Support & Training</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.reasonForSale }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.supportAndTraining }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Broker licensing</div>
|
<div class="text-500 w-full md:w-3 font-medium">Reason for Sale</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.brokerLicencing }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.reasonForSale }}</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
|
<div class="text-500 w-full md:w-3 font-medium">Broker licensing</div>
|
||||||
|
<div class="text-900 w-full md:w-9">{{ listing.brokerLicencing }}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
|
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
|
||||||
@@ -79,7 +91,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
||||||
<input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" />
|
<!-- <input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> -->
|
||||||
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="state" class="font-medium text-900">Country/State</label>
|
<label for="state" class="font-medium text-900">Country/State</label>
|
||||||
@@ -92,9 +105,9 @@
|
|||||||
</div>
|
</div>
|
||||||
@if(listingUser){
|
@if(listingUser){
|
||||||
<div class="surface-border mb-4 col-12 flex align-items-center">
|
<div class="surface-border mb-4 col-12 flex align-items-center">
|
||||||
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
||||||
@if(listingUser.hasCompanyLogo){
|
@if(listingUser.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listingUser.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 30px; max-width: 100px" />
|
<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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import { GalleriaModule } from 'primeng/galleria';
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { HistoryService } from '../../../services/history.service';
|
import { HistoryService } from '../../../services/history.service';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { MailService } from '../../../services/mail.service';
|
import { MailService } from '../../../services/mail.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import { getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
|
import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../../utils/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-details-business-listing',
|
selector: 'app-details-business-listing',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, GalleriaModule],
|
imports: [SharedModule, GalleriaModule, InputMaskModule],
|
||||||
providers: [MessageService],
|
providers: [MessageService],
|
||||||
templateUrl: './details-business-listing.component.html',
|
templateUrl: './details-business-listing.component.html',
|
||||||
styleUrl: './details-business-listing.component.scss',
|
styleUrl: './details-business-listing.component.scss',
|
||||||
@@ -49,7 +49,8 @@ export class DetailsBusinessListingComponent {
|
|||||||
criteria: ListingCriteria;
|
criteria: ListingCriteria;
|
||||||
mailinfo: MailInfo;
|
mailinfo: MailInfo;
|
||||||
environment = environment;
|
environment = environment;
|
||||||
user: KeycloakUser;
|
keycloakUser: KeycloakUser;
|
||||||
|
user: User;
|
||||||
listingUser: User;
|
listingUser: User;
|
||||||
description: SafeHtml;
|
description: SafeHtml;
|
||||||
private history: string[] = [];
|
private history: string[] = [];
|
||||||
@@ -73,11 +74,16 @@ export class DetailsBusinessListingComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
||||||
this.user;
|
|
||||||
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
|
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const token = await this.keycloakService.getToken();
|
||||||
|
this.keycloakUser = map2User(token);
|
||||||
|
if (this.keycloakUser) {
|
||||||
|
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||||
|
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
|
||||||
|
}
|
||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
||||||
this.listingUser = await this.userService.getById(this.listing.userId);
|
this.listingUser = await this.userService.getById(this.listing.userId);
|
||||||
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
||||||
|
|||||||
@@ -14,32 +14,32 @@
|
|||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium flex">Description</div>
|
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
|
||||||
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
|
<div class="text-900 w-full md:w-9 line-height-3" [innerHTML]="description"></div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Property Category</div>
|
<div class="text-500 w-full md:w-3 font-medium">Property Category</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
|
<div class="text-900 w-full md:w-9">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
|
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div>
|
<div class="text-900 w-full md:w-9">{{ selectOptions.getState(listing.state) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">City</div>
|
<div class="text-500 w-full md:w-3 font-medium">City</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.city }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.city }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Zip Code</div>
|
<div class="text-500 w-full md:w-3 font-medium">Zip Code</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.zipCode }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.zipCode }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">County</div>
|
<div class="text-500 w-full md:w-3 font-medium">County</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.county }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.county }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Asking Price:</div>
|
<div class="text-500 w-full md:w-3 font-medium">Asking Price:</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.price | currency }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<p-galleria [value]="propertyImages" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
|
<p-galleria [value]="listing.imageOrder" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
|
||||||
<ng-template pTemplate="item" let-item>
|
<ng-template pTemplate="item" let-item>
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ item }}" style="width: 100%" />
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ item }}" style="width: 100%" />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</p-galleria>
|
</p-galleria>
|
||||||
@if (mailinfo){
|
@if (mailinfo){
|
||||||
@@ -69,7 +69,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
||||||
<input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" />
|
<!-- <input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> -->
|
||||||
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="state" class="font-medium text-900">Country/State</label>
|
<label for="state" class="font-medium text-900">Country/State</label>
|
||||||
@@ -82,9 +83,9 @@
|
|||||||
</div>
|
</div>
|
||||||
@if(listingUser){
|
@if(listingUser){
|
||||||
<div class="surface-border mb-4 col-12 flex align-items-center">
|
<div class="surface-border mb-4 col-12 flex align-items-center">
|
||||||
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
||||||
@if(listingUser.hasCompanyLogo){
|
@if(listingUser.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listingUser.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 30px; max-width: 100px" />
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import { GalleriaModule } from 'primeng/galleria';
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { HistoryService } from '../../../services/history.service';
|
import { HistoryService } from '../../../services/history.service';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { MailService } from '../../../services/mail.service';
|
import { MailService } from '../../../services/mail.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
@@ -20,13 +22,12 @@ import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../.
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-details-commercial-property-listing',
|
selector: 'app-details-commercial-property-listing',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, GalleriaModule],
|
imports: [SharedModule, GalleriaModule, InputMaskModule],
|
||||||
providers: [MessageService],
|
providers: [MessageService],
|
||||||
templateUrl: './details-commercial-property-listing.component.html',
|
templateUrl: './details-commercial-property-listing.component.html',
|
||||||
styleUrl: './details-commercial-property-listing.component.scss',
|
styleUrl: './details-commercial-property-listing.component.scss',
|
||||||
})
|
})
|
||||||
export class DetailsCommercialPropertyListingComponent {
|
export class DetailsCommercialPropertyListingComponent {
|
||||||
// listings: Array<BusinessListing>;
|
|
||||||
responsiveOptions = [
|
responsiveOptions = [
|
||||||
{
|
{
|
||||||
breakpoint: '1199px',
|
breakpoint: '1199px',
|
||||||
@@ -48,9 +49,9 @@ export class DetailsCommercialPropertyListingComponent {
|
|||||||
listing: CommercialPropertyListing;
|
listing: CommercialPropertyListing;
|
||||||
criteria: ListingCriteria;
|
criteria: ListingCriteria;
|
||||||
mailinfo: MailInfo;
|
mailinfo: MailInfo;
|
||||||
propertyImages: string[] = [];
|
|
||||||
environment = environment;
|
environment = environment;
|
||||||
user: KeycloakUser;
|
keycloakUser: KeycloakUser;
|
||||||
|
user: User;
|
||||||
listingUser: User;
|
listingUser: User;
|
||||||
description: SafeHtml;
|
description: SafeHtml;
|
||||||
ts = new Date().getTime();
|
ts = new Date().getTime();
|
||||||
@@ -67,6 +68,7 @@ export class DetailsCommercialPropertyListingComponent {
|
|||||||
private sanitizer: DomSanitizer,
|
private sanitizer: DomSanitizer,
|
||||||
public historyService: HistoryService,
|
public historyService: HistoryService,
|
||||||
public keycloakService: KeycloakService,
|
public keycloakService: KeycloakService,
|
||||||
|
private imageService: ImageService,
|
||||||
) {
|
) {
|
||||||
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
||||||
|
|
||||||
@@ -75,9 +77,12 @@ export class DetailsCommercialPropertyListingComponent {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const token = await this.keycloakService.getToken();
|
const token = await this.keycloakService.getToken();
|
||||||
this.user = map2User(token);
|
this.keycloakUser = map2User(token);
|
||||||
|
if (this.keycloakUser) {
|
||||||
|
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||||
|
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
|
||||||
|
}
|
||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
this.listingUser = await this.userService.getById(this.listing.userId);
|
this.listingUser = await this.userService.getById(this.listing.userId);
|
||||||
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
|
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
|
||||||
<div class="flex align-items-start flex-column md:flex-row">
|
<div class="flex align-items-start flex-column md:flex-row">
|
||||||
@if(user.hasProfile){
|
@if(user.hasProfile){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures//profile/{{ user.id }}.avif?_ts={{ ts }}" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
<img src="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mr-5 mt-3">
|
<div class="mr-5 mt-3">
|
||||||
<span class="font-medium text-500">For Sale</span>
|
<span class="font-medium text-500">For Sale</span>
|
||||||
<div class="text-700 mt-2">{{ businessListings.length + commercialPropListings.length }}</div>
|
<div class="text-700 mt-2">{{ businessListings?.length + commercialPropListings?.length }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="mr-5 mt-3">
|
<!-- <div class="mr-5 mt-3">
|
||||||
<span class="font-medium text-500">Sold</span>
|
<span class="font-medium text-500">Sold</span>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<!-- <span class="font-medium text-500">Logo</span> -->
|
<!-- <span class="font-medium text-500">Logo</span> -->
|
||||||
<div>
|
<div>
|
||||||
@if(user.hasCompanyLogo){
|
@if(user.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ user.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 60px; max-width: 100px" />
|
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 60px; max-width: 100px" />
|
||||||
}
|
}
|
||||||
<!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png"
|
<!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png"
|
||||||
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
|
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Phone Number</div>
|
<div class="text-500 w-full md:w-2 font-medium">Phone Number</div>
|
||||||
<div class="text-900 w-full md:w-10 line-height-3">{{ user.phoneNumber }}</div>
|
<div class="text-900 w-full md:w-10 line-height-3">{{ formatPhoneNumber(user.phoneNumber) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">EMail Address</div>
|
<div class="text-500 w-full md:w-2 font-medium">EMail Address</div>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<div class="p-3 border-1 surface-border border-round surface-card">
|
<div class="p-3 border-1 surface-border border-round surface-card">
|
||||||
<div class="text-900 mb-2 flex align-items-center">
|
<div class="text-900 mb-2 flex align-items-center">
|
||||||
@if (listing.imageOrder?.length>0){
|
@if (listing.imageOrder?.length>0){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
|
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import { GalleriaModule } from 'primeng/galleria';
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { KeycloakUser, ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser, ListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { HistoryService } from '../../../services/history.service';
|
import { HistoryService } from '../../../services/history.service';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import { map2User } from '../../../utils/utils';
|
import { formatPhoneNumber, map2User } from '../../../utils/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-details-user',
|
selector: 'app-details-user',
|
||||||
@@ -37,6 +37,8 @@ export class DetailsUserComponent {
|
|||||||
offeredServices: SafeHtml;
|
offeredServices: SafeHtml;
|
||||||
ts = new Date().getTime();
|
ts = new Date().getTime();
|
||||||
env = environment;
|
env = environment;
|
||||||
|
emailToDirName = emailToDirName;
|
||||||
|
formatPhoneNumber = formatPhoneNumber;
|
||||||
constructor(
|
constructor(
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -52,8 +54,7 @@ export class DetailsUserComponent {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.user = await this.userService.getById(this.id);
|
this.user = await this.userService.getById(this.id);
|
||||||
this.user.email;
|
const results = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
const results = await Promise.all([await this.listingsService.getListingByUserId(this.id, 'business'), await this.listingsService.getListingByUserId(this.id, 'commercialProperty')]);
|
|
||||||
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
|
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
|
||||||
this.businessListings = results[0];
|
this.businessListings = results[0];
|
||||||
this.commercialPropListings = results[1];
|
this.commercialPropListings = results[1];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { CheckboxModule } from 'primeng/checkbox';
|
import { CheckboxModule } from 'primeng/checkbox';
|
||||||
@@ -9,7 +10,6 @@ import { DropdownModule } from 'primeng/dropdown';
|
|||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { StyleClassModule } from 'primeng/styleclass';
|
import { StyleClassModule } from 'primeng/styleclass';
|
||||||
import { ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
import { ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { KeycloakService } from '../../services/keycloak.service';
|
|
||||||
import { ListingsService } from '../../services/listings.service';
|
import { ListingsService } from '../../services/listings.service';
|
||||||
import { SelectOptionsService } from '../../services/select-options.service';
|
import { SelectOptionsService } from '../../services/select-options.service';
|
||||||
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../utils/utils';
|
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../utils/utils';
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full">
|
<div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full">
|
||||||
<span>
|
<span>
|
||||||
@if(user.hasProfile){
|
@if(user.hasProfile){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/profile/{{ user.id }}.avif?_ts={{ ts }}" class="w-5rem" />
|
<img src="{{ env.imageBaseUrl }}/pictures/profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-5rem" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
|
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
|
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
|
||||||
@if(user.hasCompanyLogo){
|
@if(user.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ user.id }}.avif?_ts={{ ts }}" class="rounded-image" />
|
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="rounded-image" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/placeholder.png" class="rounded-image" />
|
<img src="assets/images/placeholder.png" class="rounded-image" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
#sky-line {
|
#sky-line {
|
||||||
background-image: url(../../../../assets/images/bw-sky.jpg);
|
background-image: url(../../../../assets/images/bw-sky.jpg);
|
||||||
height: 204px;
|
height: 204px;
|
||||||
background-position: bottom;
|
background-position: bottom;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
}
|
}
|
||||||
.search{
|
.search {
|
||||||
background-color: #343F69;
|
background-color: #343f69;
|
||||||
}
|
}
|
||||||
::ng-deep p-paginator div {
|
::ng-deep p-paginator div {
|
||||||
background-color: var(--surface-200) !important;
|
background-color: var(--surface-200) !important;
|
||||||
// background-color: var(--surface-400) !important;
|
// background-color: var(--surface-400) !important;
|
||||||
}
|
}
|
||||||
.rounded-image {
|
.rounded-image {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
// width: 100px;
|
// width: 100px;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
height: 45px;
|
max-height: 45px;
|
||||||
border: 1px solid rgba(0,0,0,0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
padding: 1px 1px;
|
padding: 1px 1px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { PaginatorModule } from 'primeng/paginator';
|
|||||||
import { StyleClassModule } from 'primeng/styleclass';
|
import { StyleClassModule } from 'primeng/styleclass';
|
||||||
import { ToggleButtonModule } from 'primeng/togglebutton';
|
import { ToggleButtonModule } from 'primeng/togglebutton';
|
||||||
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
@@ -60,7 +60,7 @@ export class BrokerListingsComponent {
|
|||||||
ts = new Date().getTime();
|
ts = new Date().getTime();
|
||||||
env = environment;
|
env = environment;
|
||||||
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
||||||
|
emailToDirName = emailToDirName;
|
||||||
constructor(
|
constructor(
|
||||||
public selectOptions: SelectOptionsService,
|
public selectOptions: SelectOptionsService,
|
||||||
private listingsService: ListingsService,
|
private listingsService: ListingsService,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
@for (listing of listings; track listing.id) {
|
@for (listing of listings; track listing.id) {
|
||||||
<div class="col-12 lg:col-3 p-3">
|
<div class="col-12 lg:col-3 p-3">
|
||||||
<div class="shadow-2 border-round surface-card h-full flex-column justify-content-between flex">
|
<div class="shadow-2 border-round surface-card h-full flex-column justify-content-between flex">
|
||||||
<div class="p-4 flex flex-column relative">
|
<div class="p-4 flex flex-column relative h-full">
|
||||||
<div class="flex align-items-center">
|
<div class="flex align-items-center">
|
||||||
<span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">
|
<span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">
|
||||||
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
|
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{ listing.established }}</p>
|
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{ listing.established }}</p>
|
||||||
<div class="icon-pos">
|
<div class="icon-pos">
|
||||||
<a routerLink="/details-user/{{ listing.userId }}" class="mr-2"
|
<a routerLink="/details-user/{{ listing.userId }}" class="mr-2"
|
||||||
><img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.userId }}.avif?_ts={{ ts }}" (error)="imageErrorHandler(listing)" class="rounded-image"
|
><img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" (error)="imageErrorHandler(listing)" class="rounded-image"
|
||||||
/></a>
|
/></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { StyleClassModule } from 'primeng/styleclass';
|
|||||||
import { ToggleButtonModule } from 'primeng/togglebutton';
|
import { ToggleButtonModule } from 'primeng/togglebutton';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
@@ -58,7 +58,7 @@ export class BusinessListingsComponent {
|
|||||||
rows: number = 12;
|
rows: number = 12;
|
||||||
env = environment;
|
env = environment;
|
||||||
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
||||||
|
emailToDirName = emailToDirName;
|
||||||
constructor(
|
constructor(
|
||||||
public selectOptions: SelectOptionsService,
|
public selectOptions: SelectOptionsService,
|
||||||
private listingsService: ListingsService,
|
private listingsService: ListingsService,
|
||||||
|
|||||||
@@ -51,7 +51,11 @@
|
|||||||
<article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card">
|
<article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@if (listing.imageOrder?.length>0){
|
@if (listing.imageOrder?.length>0){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
|
<img
|
||||||
|
src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}"
|
||||||
|
alt="Image"
|
||||||
|
class="border-round w-full h-full md:w-12rem md:h-9rem"
|
||||||
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
|
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
|||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||||
import { faRightFromBracket } from '@fortawesome/free-solid-svg-icons';
|
import { faRightFromBracket } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { DividerModule } from 'primeng/divider';
|
import { DividerModule } from 'primeng/divider';
|
||||||
import { RippleModule } from 'primeng/ripple';
|
import { RippleModule } from 'primeng/ripple';
|
||||||
import { StyleClassModule } from 'primeng/styleclass';
|
import { StyleClassModule } from 'primeng/styleclass';
|
||||||
import { KeycloakService } from '../../services/keycloak.service';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'menu-account',
|
selector: 'menu-account',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { KeycloakService } from '../../services/keycloak.service';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { SharedModule } from '../../shared/shared/shared.module';
|
import { SharedModule } from '../../shared/shared/shared.module';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -8,10 +8,16 @@
|
|||||||
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
||||||
<div class="flex-auto p-fluid">
|
<div class="flex-auto p-fluid">
|
||||||
@if (user){
|
@if (user){
|
||||||
<div class="mb-4">
|
<div class="grid">
|
||||||
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email" />
|
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
|
||||||
<p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at support@bizmatch.net</p>
|
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email" />
|
||||||
|
<p class="font-italic text-xs line-height-1">You can only modify your email by contacting us at support@bizmatch.net</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4 col-12 md:col-6">
|
||||||
|
<label for="customerType" class="block font-medium text-900 mb-2">Customer Type</label>
|
||||||
|
<p-dropdown id="customerType" [options]="selectOptions?.customerTypes" [(ngModel)]="user.customerType" optionLabel="name" optionValue="value" placeholder="State" [style]="{ width: '100%' }"></p-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
@@ -23,6 +29,7 @@
|
|||||||
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" />
|
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (isProfessional){
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<label for="firstname" class="block font-medium text-900 mb-2">Company Name</label>
|
<label for="firstname" class="block font-medium text-900 mb-2">Company Name</label>
|
||||||
@@ -33,10 +40,11 @@
|
|||||||
<input id="lastname" type="text" pInputText [(ngModel)]="user.description" />
|
<input id="lastname" type="text" pInputText [(ngModel)]="user.description" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} @if (isProfessional){
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-4">
|
<div class="mb-4 col-12 md:col-4">
|
||||||
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
|
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
|
||||||
<input id="phoneNumber" type="text" pInputText [(ngModel)]="user.phoneNumber" />
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="user.phoneNumber"></p-inputMask>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 col-12 md:col-4">
|
<div class="mb-4 col-12 md:col-4">
|
||||||
<label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label>
|
<label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label>
|
||||||
@@ -47,6 +55,14 @@
|
|||||||
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
|
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="grid">
|
||||||
|
<div class="mb-4 col-12 md:col-6">
|
||||||
|
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
|
||||||
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="user.phoneNumber"></p-inputMask>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @if (isProfessional){
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
|
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
|
||||||
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules">
|
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules">
|
||||||
@@ -59,7 +75,6 @@
|
|||||||
<ng-template pTemplate="header"></ng-template>
|
<ng-template pTemplate="header"></ng-template>
|
||||||
</p-editor>
|
</p-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
|
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
|
||||||
@for (areasServed of user.areasServed; track areasServed){
|
@for (areasServed of user.areasServed; track areasServed){
|
||||||
@@ -118,11 +133,12 @@
|
|||||||
<span class="text-xs"> (Add more licenses or remove existing ones.)</span>
|
<span class="text-xs"> (Add more licenses or remove existing ones.)</span>
|
||||||
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
|
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
|
||||||
</div>
|
</div>
|
||||||
}
|
} }
|
||||||
<div>
|
<div>
|
||||||
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
|
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (isProfessional){
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-column align-items-center flex-or mb-8">
|
<div class="flex flex-column align-items-center flex-or mb-8">
|
||||||
<span class="font-medium text-900 mb-2">Company Logo</span>
|
<span class="font-medium text-900 mb-2">Company Logo</span>
|
||||||
@@ -172,6 +188,7 @@
|
|||||||
></p-fileUpload>
|
></p-fileUpload>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
|
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
|
||||||
<p-divider></p-divider>
|
<p-divider></p-divider>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
/* Stil für das FontAwesome Icon */
|
/* Stil für das FontAwesome Icon */
|
||||||
.image-wrap fa-icon {
|
.image-wrap fa-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px; /* Positioniert das Icon am oberen Rand des Bildes */
|
top: -5px; /* Positioniert das Icon am oberen Rand des Bildes */
|
||||||
right: 5px; /* Positioniert das Icon am rechten Rand des Bildes */
|
right: -18px; /* Positioniert das Icon am rechten Rand des Bildes */
|
||||||
color: #fff; /* Weiße Farbe für das Icon */
|
color: #fff; /* Weiße Farbe für das Icon */
|
||||||
background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */
|
background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */
|
||||||
padding: 5px; /* Ein wenig Platz um das Icon */
|
padding: 5px; /* Ein wenig Platz um das Icon */
|
||||||
|
|||||||
@@ -3,32 +3,33 @@ import { ChangeDetectorRef, Component, ViewChild } from '@angular/core';
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { AngularCropperjsModule } from 'angular-cropperjs';
|
import { AngularCropperjsModule } from 'angular-cropperjs';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
import { DialogModule } from 'primeng/dialog';
|
import { DialogModule } from 'primeng/dialog';
|
||||||
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { EditorModule } from 'primeng/editor';
|
import { EditorModule } from 'primeng/editor';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
import { SelectButtonModule } from 'primeng/selectbutton';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, Invoice, Subscription } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, Invoice, Subscription, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
|
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
|
||||||
import { GeoService } from '../../../services/geo.service';
|
import { GeoService } from '../../../services/geo.service';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
|
||||||
import { LoadingService } from '../../../services/loading.service';
|
import { LoadingService } from '../../../services/loading.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { SubscriptionsService } from '../../../services/subscriptions.service';
|
import { SubscriptionsService } from '../../../services/subscriptions.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import { map2User } from '../../../utils/utils';
|
import { getDialogWidth, getImageDimensions, map2User } from '../../../utils/utils';
|
||||||
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-account',
|
selector: 'app-account',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule, ConfirmDialogModule],
|
imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule, ConfirmDialogModule, InputMaskModule],
|
||||||
providers: [MessageService, DialogService, ConfirmationService],
|
providers: [MessageService, DialogService, ConfirmationService],
|
||||||
templateUrl: './account.component.html',
|
templateUrl: './account.component.html',
|
||||||
styleUrl: './account.component.scss',
|
styleUrl: './account.component.scss',
|
||||||
@@ -74,29 +75,29 @@ export class AccountComponent {
|
|||||||
try {
|
try {
|
||||||
this.user = await this.userService.getByMail(email);
|
this.user = await this.userService.getByMail(email);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.user = { email, firstname: keycloakUser.firstName, lastname: keycloakUser.lastName, areasServed: [], licensedIn: [], companyOverview: '', offeredServices: '' };
|
this.user = { email, firstname: keycloakUser.firstName, lastname: keycloakUser.lastName, areasServed: [], licensedIn: [], companyOverview: '', offeredServices: '', customerType: 'broker' };
|
||||||
this.user = await this.userService.save(this.user);
|
this.user = await this.userService.save(this.user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
|
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
|
||||||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
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/${this.user.id}.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`;
|
||||||
}
|
}
|
||||||
printInvoice(invoice: Invoice) {}
|
printInvoice(invoice: Invoice) {}
|
||||||
|
|
||||||
async updateProfile(user: User) {
|
async updateProfile(user: User) {
|
||||||
await this.userService.save(this.user);
|
await this.userService.save(this.user);
|
||||||
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Acount changes have been persisted', life: 3000 });
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Account changes have been persisted', life: 3000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
onUploadCompanyLogo(event: any) {
|
onUploadCompanyLogo(event: any) {
|
||||||
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
||||||
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}${uniqueSuffix}`;
|
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}${uniqueSuffix}`;
|
||||||
}
|
}
|
||||||
onUploadProfilePicture(event: any) {
|
onUploadProfilePicture(event: any) {
|
||||||
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
||||||
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}${uniqueSuffix}`;
|
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}${uniqueSuffix}`;
|
||||||
}
|
}
|
||||||
setImageToFallback(event: Event) {
|
setImageToFallback(event: Event) {
|
||||||
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
|
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
|
||||||
@@ -120,50 +121,53 @@ export class AccountComponent {
|
|||||||
removeArea() {
|
removeArea() {
|
||||||
this.user.areasServed.splice(this.user.areasServed.length - 1, 1);
|
this.user.areasServed.splice(this.user.areasServed.length - 1, 1);
|
||||||
}
|
}
|
||||||
|
get isProfessional() {
|
||||||
|
return this.user.customerType === 'broker' || this.user.customerType === 'professional';
|
||||||
|
}
|
||||||
select(event: any, type: 'company' | 'profile') {
|
select(event: any, type: 'company' | 'profile') {
|
||||||
const imageUrl = URL.createObjectURL(event.files[0]);
|
const imageUrl = URL.createObjectURL(event.files[0]);
|
||||||
this.type = type;
|
this.type = type;
|
||||||
const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value };
|
const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value };
|
||||||
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
getImageDimensions(imageUrl).then(dimensions => {
|
||||||
data: {
|
const dialogWidth = getDialogWidth(dimensions);
|
||||||
imageUrl: imageUrl,
|
|
||||||
fileUpload: type === 'company' ? this.companyUpload : this.profileUpload,
|
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
||||||
config: config,
|
data: {
|
||||||
ratioVariable: type === 'company' ? true : false,
|
imageUrl: imageUrl,
|
||||||
},
|
fileUpload: type === 'company' ? this.companyUpload : this.profileUpload,
|
||||||
header: 'Edit Image',
|
config: config,
|
||||||
width: '50vw',
|
ratioVariable: type === 'company' ? true : false,
|
||||||
modal: true,
|
},
|
||||||
closeOnEscape: true,
|
header: 'Edit Image',
|
||||||
keepInViewport: true,
|
width: dialogWidth,
|
||||||
closable: false,
|
modal: true,
|
||||||
breakpoints: {
|
closeOnEscape: true,
|
||||||
'960px': '75vw',
|
keepInViewport: true,
|
||||||
'640px': '90vw',
|
closable: false,
|
||||||
},
|
});
|
||||||
});
|
this.dialogRef.onClose.subscribe(cropper => {
|
||||||
this.dialogRef.onClose.subscribe(cropper => {
|
if (cropper) {
|
||||||
if (cropper) {
|
this.loadingService.startLoading('uploadImage');
|
||||||
this.loadingService.startLoading('uploadImage');
|
cropper.getCroppedCanvas().toBlob(async blob => {
|
||||||
cropper.getCroppedCanvas().toBlob(async blob => {
|
this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', emailToDirName(this.user.email)).subscribe(
|
||||||
this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', this.user.id).subscribe(
|
async event => {
|
||||||
async event => {
|
if (event.type === HttpEventType.Response) {
|
||||||
if (event.type === HttpEventType.Response) {
|
this.loadingService.stopLoading('uploadImage');
|
||||||
this.loadingService.stopLoading('uploadImage');
|
if (this.type === 'company') {
|
||||||
if (this.type === 'company') {
|
this.user.hasCompanyLogo = true; //
|
||||||
this.user.hasCompanyLogo = true; //
|
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
|
||||||
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}.avif?_ts=${new Date().getTime()}`;
|
} else {
|
||||||
} else {
|
this.user.hasProfile = true;
|
||||||
this.user.hasProfile = true;
|
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
|
||||||
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}.avif?_ts=${new Date().getTime()}`;
|
}
|
||||||
|
await this.userService.save(this.user);
|
||||||
}
|
}
|
||||||
await this.userService.save(this.user);
|
},
|
||||||
}
|
error => console.error('Fehler beim Upload:', error),
|
||||||
},
|
);
|
||||||
error => console.error('Fehler beim Upload:', error),
|
});
|
||||||
);
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
deleteConfirm(type: 'profile' | 'logo') {
|
deleteConfirm(type: 'profile' | 'logo') {
|
||||||
@@ -180,10 +184,10 @@ export class AccountComponent {
|
|||||||
accept: async () => {
|
accept: async () => {
|
||||||
if (type === 'profile') {
|
if (type === 'profile') {
|
||||||
this.user.hasProfile = false;
|
this.user.hasProfile = false;
|
||||||
await Promise.all([this.imageService.deleteProfileImagesById(this.user.id), this.userService.save(this.user)]);
|
await Promise.all([this.imageService.deleteProfileImagesById(this.user.email), this.userService.save(this.user)]);
|
||||||
} else {
|
} else {
|
||||||
this.user.hasCompanyLogo = false;
|
this.user.hasCompanyLogo = false;
|
||||||
await Promise.all([this.imageService.deleteLogoImagesById(this.user.id), this.userService.save(this.user)]);
|
await Promise.all([this.imageService.deleteLogoImagesById(this.user.email), this.userService.save(this.user)]);
|
||||||
}
|
}
|
||||||
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
||||||
this.user = await this.userService.getById(this.user.id);
|
this.user = await this.userService.getById(this.user.id);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
|||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { AngularCropperjsModule } from 'angular-cropperjs';
|
import { AngularCropperjsModule } from 'angular-cropperjs';
|
||||||
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
|
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { CarouselModule } from 'primeng/carousel';
|
import { CarouselModule } from 'primeng/carousel';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
@@ -17,13 +18,12 @@ import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dy
|
|||||||
import { EditorModule } from 'primeng/editor';
|
import { EditorModule } from 'primeng/editor';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
||||||
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
||||||
import { GeoService } from '../../../services/geo.service';
|
import { GeoService } from '../../../services/geo.service';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
|
||||||
import { LoadingService } from '../../../services/loading.service';
|
import { LoadingService } from '../../../services/loading.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
@@ -60,9 +60,7 @@ export class EditBusinessListingComponent {
|
|||||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||||
user: User;
|
user: User;
|
||||||
maxFileSize = 3000000;
|
maxFileSize = 3000000;
|
||||||
uploadUrl: string;
|
|
||||||
environment = environment;
|
environment = environment;
|
||||||
propertyImages: string[];
|
|
||||||
responsiveOptions = [
|
responsiveOptions = [
|
||||||
{
|
{
|
||||||
breakpoint: '1199px',
|
breakpoint: '1199px',
|
||||||
@@ -123,14 +121,14 @@ export class EditBusinessListingComponent {
|
|||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
||||||
} else {
|
} else {
|
||||||
this.listing = createDefaultBusinessListing();
|
this.listing = createDefaultBusinessListing();
|
||||||
this.listing.userId = await this.userService.getId(keycloakUser.email);
|
const listingUser = await this.userService.getByMail(keycloakUser.email);
|
||||||
|
this.listing.userId = listingUser.id;
|
||||||
|
this.listing.imageName = emailToDirName(keycloakUser.email);
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.listing.title = this.data?.title;
|
this.listing.title = this.data?.title;
|
||||||
this.listing.description = this.data?.description;
|
this.listing.description = this.data?.description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
|
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
|
|||||||
@@ -82,58 +82,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p-divider></p-divider>
|
<div class="grid">
|
||||||
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<div class="flex-auto p-fluid">
|
<p-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
|
||||||
<div class="grid">
|
<span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
|
||||||
<div class="mb-4 col-12 md:col-6">
|
</div>
|
||||||
<label for="price" class="block font-medium text-900 mb-2">Price</label>
|
</div>
|
||||||
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
|
<div>
|
||||||
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
|
<p-divider></p-divider>
|
||||||
</div>
|
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="flex-auto p-fluid">
|
||||||
<div class="flex flex-column align-items-center flex-or">
|
<div class="grid">
|
||||||
<span class="font-medium text-900 mb-2">Property Pictures</span>
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
|
<label for="price" class="block font-medium text-900 mb-2">Price</label>
|
||||||
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
|
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
|
||||||
<p-fileUpload
|
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
|
||||||
mode="basic"
|
</div>
|
||||||
chooseLabel="Upload"
|
<div class="mb-4 col-12 md:col-6">
|
||||||
[customUpload]="true"
|
<div class="flex flex-column align-items-center flex-or">
|
||||||
name="file"
|
<span class="font-medium text-900 mb-2">Property Pictures</span>
|
||||||
accept="image/*"
|
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
|
||||||
[maxFileSize]="maxFileSize"
|
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
|
||||||
(onSelect)="select($event)"
|
<p-fileUpload
|
||||||
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
|
mode="basic"
|
||||||
[disabled]="!listing.id"
|
chooseLabel="Upload"
|
||||||
>
|
[customUpload]="true"
|
||||||
</p-fileUpload>
|
name="file"
|
||||||
|
accept="image/*"
|
||||||
|
[maxFileSize]="maxFileSize"
|
||||||
|
(onSelect)="select($event)"
|
||||||
|
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
|
||||||
|
[disabled]="!listing.id"
|
||||||
|
>
|
||||||
|
</p-fileUpload>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@if (listing && listing.imageOrder?.length>0){
|
||||||
@if (propertyImages?.length>0){
|
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal">
|
||||||
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal">
|
@for (image of listing.imageOrder; track listing.imageOrder) {
|
||||||
@for (image of propertyImages; track image) {
|
<span cdkDropList mixedCdkDropList>
|
||||||
<span cdkDropList mixedCdkDropList>
|
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
|
||||||
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag />
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag />
|
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
|
||||||
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
|
</div>
|
||||||
</div>
|
</span>
|
||||||
</span>
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
@if (mode==='create'){
|
|
||||||
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
|
|
||||||
} @else {
|
|
||||||
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
|
|
||||||
}
|
}
|
||||||
|
<div>
|
||||||
|
@if (mode==='create'){
|
||||||
|
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
|
||||||
|
} @else {
|
||||||
|
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p-toast></p-toast>
|
||||||
|
<p-confirmDialog></p-confirmDialog>
|
||||||
</div>
|
</div>
|
||||||
<p-toast></p-toast>
|
|
||||||
<p-confirmDialog></p-confirmDialog>
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
|||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { createDefaultCommercialPropertyListing, map2User, routeListingWithState } from '../../../utils/utils';
|
import { createDefaultCommercialPropertyListing, getDialogWidth, getImageDimensions, map2User, routeListingWithState } from '../../../utils/utils';
|
||||||
|
|
||||||
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { HttpEventType } from '@angular/common/http';
|
import { HttpEventType } from '@angular/common/http';
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { AngularCropperjsModule } from 'angular-cropperjs';
|
import { AngularCropperjsModule } from 'angular-cropperjs';
|
||||||
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
|
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { CarouselModule } from 'primeng/carousel';
|
import { CarouselModule } from 'primeng/carousel';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
@@ -17,16 +18,14 @@ import { DialogModule } from 'primeng/dialog';
|
|||||||
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { EditorModule } from 'primeng/editor';
|
import { EditorModule } from 'primeng/editor';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
|
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
|
||||||
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
||||||
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
||||||
import { GeoService } from '../../../services/geo.service';
|
import { GeoService } from '../../../services/geo.service';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
|
||||||
import { LoadingService } from '../../../services/loading.service';
|
import { LoadingService } from '../../../services/loading.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
@@ -63,9 +62,7 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||||
user: User;
|
user: User;
|
||||||
maxFileSize = 3000000;
|
maxFileSize = 3000000;
|
||||||
uploadUrl: string;
|
|
||||||
environment = environment;
|
environment = environment;
|
||||||
propertyImages: string[];
|
|
||||||
responsiveOptions = [
|
responsiveOptions = [
|
||||||
{
|
{
|
||||||
breakpoint: '1199px',
|
breakpoint: '1199px',
|
||||||
@@ -131,15 +128,14 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
} else {
|
} else {
|
||||||
this.listing = createDefaultCommercialPropertyListing();
|
this.listing = createDefaultCommercialPropertyListing();
|
||||||
this.listing.userId = await this.userService.getId(keycloakUser.email);
|
const listingUser = await this.userService.getByMail(keycloakUser.email);
|
||||||
this.listing.imagePath = uuidv4();
|
this.listing.userId = listingUser.id;
|
||||||
|
this.listing.imagePath = `${emailToDirName(keycloakUser.email)}`;
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.listing.title = this.data?.title;
|
this.listing.title = this.data?.title;
|
||||||
this.listing.description = this.data?.description;
|
this.listing.description = this.data?.description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
|
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
@@ -155,40 +151,44 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
|
|
||||||
select(event: any) {
|
select(event: any) {
|
||||||
const imageUrl = URL.createObjectURL(event.files[0]);
|
const imageUrl = URL.createObjectURL(event.files[0]);
|
||||||
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
getImageDimensions(imageUrl).then(dimensions => {
|
||||||
data: {
|
const dialogWidth = getDialogWidth(dimensions);
|
||||||
imageUrl: imageUrl,
|
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
||||||
fileUpload: this.fileUpload,
|
data: {
|
||||||
ratioVariable: false,
|
imageUrl: imageUrl,
|
||||||
},
|
fileUpload: this.fileUpload,
|
||||||
header: 'Edit Image',
|
ratioVariable: false,
|
||||||
width: '50vw',
|
},
|
||||||
modal: true,
|
header: 'Edit Image',
|
||||||
closeOnEscape: true,
|
width: dialogWidth,
|
||||||
keepInViewport: true,
|
modal: true,
|
||||||
closable: false,
|
closeOnEscape: true,
|
||||||
breakpoints: {
|
keepInViewport: true,
|
||||||
'960px': '75vw',
|
closable: false,
|
||||||
'640px': '90vw',
|
breakpoints: {
|
||||||
},
|
'960px': '75vw',
|
||||||
});
|
'640px': '90vw',
|
||||||
this.dialogRef.onClose.subscribe(cropper => {
|
},
|
||||||
if (cropper) {
|
});
|
||||||
this.loadingService.startLoading('uploadImage');
|
this.dialogRef.onClose.subscribe(cropper => {
|
||||||
cropper.getCroppedCanvas().toBlob(async blob => {
|
if (cropper) {
|
||||||
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath).subscribe(
|
this.loadingService.startLoading('uploadImage');
|
||||||
async event => {
|
cropper.getCroppedCanvas().toBlob(async blob => {
|
||||||
if (event.type === HttpEventType.Response) {
|
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
|
||||||
console.log('Upload abgeschlossen', event.body);
|
async event => {
|
||||||
this.loadingService.stopLoading('uploadImage');
|
if (event.type === HttpEventType.Response) {
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
this.ts = new Date().getTime();
|
||||||
}
|
console.log('Upload abgeschlossen', event.body);
|
||||||
},
|
this.loadingService.stopLoading('uploadImage');
|
||||||
error => console.error('Fehler beim Upload:', error),
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
);
|
}
|
||||||
}, 'image/jpg');
|
},
|
||||||
cropper.destroy();
|
error => console.error('Fehler beim Upload:', error),
|
||||||
}
|
);
|
||||||
|
}, 'image/jpg');
|
||||||
|
cropper.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,9 +205,8 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
|
|
||||||
accept: async () => {
|
accept: async () => {
|
||||||
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
|
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
|
||||||
await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
|
await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
|
||||||
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
},
|
},
|
||||||
reject: () => {
|
reject: () => {
|
||||||
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
|
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
|
||||||
@@ -217,8 +216,7 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDrop(event: { previousIndex: number; currentIndex: number }) {
|
onDrop(event: { previousIndex: number; currentIndex: number }) {
|
||||||
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
|
moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
|
||||||
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages);
|
|
||||||
}
|
}
|
||||||
changeListingCategory(value: 'business' | 'commercialProperty') {
|
changeListingCategory(value: 'business' | 'commercialProperty') {
|
||||||
routeListingWithState(this.router, value, this.listing);
|
routeListingWithState(this.router, value, this.listing);
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
|
|
||||||
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
|
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
|
||||||
<div class="p-fluid flex flex-column lg:flex-row">
|
<div class="p-fluid flex flex-column lg:flex-row">
|
||||||
<menu-account></menu-account>
|
<menu-account></menu-account>
|
||||||
<div class="surface-card p-5 shadow-2 border-round flex-auto">
|
<div class="surface-card p-5 shadow-2 border-round flex-auto">
|
||||||
<div class="text-900 font-semibold text-lg mt-3">Contact Us</div>
|
<div class="text-900 font-semibold text-lg mt-3">Contact Us</div>
|
||||||
<p-divider></p-divider>
|
<p-divider></p-divider>
|
||||||
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
||||||
<div class="flex-auto p-fluid">
|
<div class="flex-auto p-fluid formgrid">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<label for="name" class="block font-medium text-900 mb-2">Your name</label>
|
<label for="name" class="block font-medium text-900 mb-2">Your name</label>
|
||||||
<input id="name" type="text" pInputText>
|
<input [ngClass]="{ 'ng-invalid': containsError('name'), 'ng-dirty': containsError('name') }" name="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
|
||||||
</div>
|
|
||||||
<div class="mb-4 col-12 md:col-6">
|
|
||||||
<label for="email" class="block font-medium text-900 mb-2">Your Email</label>
|
|
||||||
<input id="email" type="text" pInputText>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="phone" class="block font-medium text-900 mb-2">Your Phone</label>
|
|
||||||
<input id="phone" type="text" pInputText>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="help" class="block font-medium text-900 mb-2">How can we help you ?</label>
|
|
||||||
<textarea id="help" type="text" pInputTextarea rows="5" [autoResize]="true"></textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button pButton pRipple label="Submit" class="w-auto"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-4 col-12 md:col-6">
|
||||||
|
<label for="email" class="block font-medium text-900 mb-2">Your Email</label>
|
||||||
|
<input name="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone</label>
|
||||||
|
<p-inputMask id="phoneNumber" name="phoneNumber" mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="help" class="block font-medium text-900 mb-2">How can we help you ?</label>
|
||||||
|
<textarea name="help" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="mailinfo.sender.comments"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button pButton pRipple label="Submit" class="w-auto" (click)="mail()"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,50 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { CheckboxModule } from 'primeng/checkbox';
|
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { MessageService } from 'primeng/api';
|
||||||
import { StyleClassModule } from 'primeng/styleclass';
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { DropdownModule } from 'primeng/dropdown';
|
import { ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { CommonModule } from '@angular/common';
|
import { MailService } from '../../../services/mail.service';
|
||||||
import { ToggleButtonModule } from 'primeng/togglebutton';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import data from '../../../../assets/data/user.json';
|
import { map2User } from '../../../utils/utils';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { InputTextareaModule } from 'primeng/inputtextarea';
|
|
||||||
import { ChipModule } from 'primeng/chip';
|
|
||||||
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
|
|
||||||
import { DividerModule } from 'primeng/divider';
|
|
||||||
import { TableModule } from 'primeng/table';
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-email-us',
|
selector: 'app-email-us',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, StyleClassModule, MenuAccountComponent, DividerModule,ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule],
|
imports: [SharedModule, InputMaskModule],
|
||||||
|
providers: [MessageService],
|
||||||
templateUrl: './email-us.component.html',
|
templateUrl: './email-us.component.html',
|
||||||
styleUrl: './email-us.component.scss'
|
styleUrl: './email-us.component.scss',
|
||||||
})
|
})
|
||||||
export class EmailUsComponent {
|
export class EmailUsComponent {
|
||||||
|
mailinfo: MailInfo;
|
||||||
|
keycloakUser: KeycloakUser;
|
||||||
|
user: User;
|
||||||
|
errorResponse: ErrorResponse;
|
||||||
|
constructor(private mailService: MailService, private userService: UserService, public keycloakService: KeycloakService, private messageService: MessageService) {
|
||||||
|
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
||||||
|
}
|
||||||
|
async ngOnInit() {
|
||||||
|
const token = await this.keycloakService.getToken();
|
||||||
|
this.keycloakUser = map2User(token);
|
||||||
|
if (this.keycloakUser) {
|
||||||
|
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||||
|
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async mail() {
|
||||||
|
this.mailinfo.email = 'support@bizmatch.net';
|
||||||
|
const result = await this.mailService.mail(this.mailinfo);
|
||||||
|
if (result) {
|
||||||
|
this.errorResponse = result as ErrorResponse;
|
||||||
|
} else {
|
||||||
|
this.errorResponse = null;
|
||||||
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your request has been forwarded to the support team of bizmatch.', life: 3000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containsError(fieldname: string) {
|
||||||
|
return this.errorResponse?.fields.map(f => f.fieldname).includes(fieldname);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { KeycloakUser, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ChangeDetectorRef, Component } from '@angular/core';
|
import { ChangeDetectorRef, Component } from '@angular/core';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { ConfirmationService, MessageService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { KeycloakService } from '../../../services/keycloak.service';
|
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
@@ -36,7 +36,7 @@ export class MyListingComponent {
|
|||||||
const keycloakUser = map2User(token);
|
const keycloakUser = map2User(token);
|
||||||
const email = keycloakUser.email;
|
const email = keycloakUser.email;
|
||||||
this.user = await this.userService.getByMail(email);
|
this.user = await this.userService.getByMail(email);
|
||||||
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]);
|
const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
this.myListings = [...result[0], ...result[1]];
|
this.myListings = [...result[0], ...result[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export class MyListingComponent {
|
|||||||
} else {
|
} else {
|
||||||
await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath);
|
await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath);
|
||||||
}
|
}
|
||||||
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]);
|
const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
this.myListings = [...result[0], ...result[1]];
|
this.myListings = [...result[0], ...result[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { ImageType } from '../../../../bizmatch-server/src/models/main.model';
|
import { emailToDirName } from '../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -12,33 +12,29 @@ export class ImageService {
|
|||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string) {
|
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
|
||||||
const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
|
let uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
|
||||||
|
if (type === 'uploadPropertyPicture') {
|
||||||
|
uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}/${serialId}`;
|
||||||
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', imageBlob, 'image.png');
|
formData.append('file', imageBlob, 'image.png');
|
||||||
|
|
||||||
return this.http.post(uploadUrl, formData, {
|
return this.http.post(uploadUrl, formData, {
|
||||||
// headers: this.headers,
|
|
||||||
//reportProgress: true,
|
|
||||||
observe: 'events',
|
observe: 'events',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async deleteUserImage(userid: string, type: ImageType, name?: string) {
|
|
||||||
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/${type.delete}${userid}`));
|
async deleteListingImage(imagePath: string, serial: number, name?: string) {
|
||||||
|
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${imagePath}/${serial}/${name}`));
|
||||||
}
|
}
|
||||||
async deleteListingImage(imagePath: string, name?: string) {
|
|
||||||
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${imagePath}/${name}`));
|
async deleteLogoImagesById(email: string) {
|
||||||
|
const adjustedEmail = emailToDirName(email);
|
||||||
|
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/logo/${adjustedEmail}`));
|
||||||
}
|
}
|
||||||
async getProfileImagesForUsers(userids: string[]) {
|
async deleteProfileImagesById(email: string) {
|
||||||
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/profileImages/${userids.join(',')}`));
|
const adjustedEmail = emailToDirName(email);
|
||||||
}
|
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${adjustedEmail}`));
|
||||||
async getCompanyLogosForUsers(userids: string[]) {
|
|
||||||
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/companyLogos/${userids.join(',')}`));
|
|
||||||
}
|
|
||||||
async deleteLogoImagesById(userid: string) {
|
|
||||||
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/logo/${userid}`));
|
|
||||||
}
|
|
||||||
async deleteProfileImagesById(userid: string) {
|
|
||||||
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${userid}`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,64 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { createLogger } from '../utils/utils';
|
import { createLogger } from '../utils/utils';
|
||||||
import { KeycloakService } from './keycloak.service';
|
|
||||||
const logger = createLogger('KeycloakInitializerService');
|
const logger = createLogger('KeycloakInitializerService');
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class KeycloakInitializerService {
|
export class KeycloakInitializerService {
|
||||||
private initialized = false;
|
public initialized = false;
|
||||||
|
|
||||||
constructor(private keycloakService: KeycloakService) {}
|
constructor(private keycloakService: KeycloakService) {}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<boolean> {
|
||||||
if (this.initialized) {
|
return new Promise<boolean>(async (resolve, reject) => {
|
||||||
return;
|
try {
|
||||||
}
|
await this.keycloakService.init({
|
||||||
|
config: {
|
||||||
const authenticated = await this.keycloakService.init({
|
url: environment.keycloak.url,
|
||||||
config: {
|
realm: environment.keycloak.realm,
|
||||||
url: environment.keycloak.url,
|
clientId: environment.keycloak.clientId,
|
||||||
realm: environment.keycloak.realm,
|
},
|
||||||
clientId: environment.keycloak.clientId,
|
initOptions: {
|
||||||
},
|
onLoad: 'check-sso',
|
||||||
initOptions: {
|
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||||
onLoad: 'check-sso',
|
// flow: 'implicit',
|
||||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
},
|
||||||
},
|
});
|
||||||
|
this.initialized = true;
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
// if (this.initialized) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// logger.info(`###>calling keycloakService init ...`);
|
||||||
|
// const authenticated = await this.keycloakService.init({
|
||||||
|
// config: {
|
||||||
|
// url: environment.keycloak.url,
|
||||||
|
// realm: environment.keycloak.realm,
|
||||||
|
// clientId: environment.keycloak.clientId,
|
||||||
|
// },
|
||||||
|
// initOptions: {
|
||||||
|
// onLoad: 'check-sso',
|
||||||
|
// silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||||
|
// // flow: 'implicit',
|
||||||
|
// },
|
||||||
|
// // initOptions: {
|
||||||
|
// // pkceMethod: 'S256',
|
||||||
|
// // redirectUri: environment.keycloak.redirectUri,
|
||||||
|
// // checkLoginIframe: false,
|
||||||
|
// // },
|
||||||
|
// });
|
||||||
|
// logger.info(`+++>authenticated: ${authenticated}`);
|
||||||
|
// const token = await this.keycloakService.getToken();
|
||||||
|
// logger.info(`--->${token}`);
|
||||||
|
|
||||||
logger.info(`--->${authenticated}`);
|
// this.initialized = true;
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isInitialized(): boolean {
|
// isInitialized(): boolean {
|
||||||
return this.initialized;
|
// return this.initialized;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,541 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright Mauricio Gemelli Vigolo and contributors.
|
|
||||||
*
|
|
||||||
* Use of this source code is governed by a MIT-style license that can be
|
|
||||||
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { HttpHeaders, HttpRequest } from '@angular/common/http';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import Keycloak from 'keycloak-js';
|
|
||||||
import { Subject, from } from 'rxjs';
|
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { KeycloakEvent, KeycloakEventType } from '../models/keycloak-event';
|
|
||||||
import { ExcludedUrl, ExcludedUrlRegex, KeycloakOptions } from '../models/keycloak-options';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service to expose existent methods from the Keycloak JS adapter, adding new
|
|
||||||
* functionalities to improve the use of keycloak in Angular v > 4.3 applications.
|
|
||||||
*
|
|
||||||
* This class should be injected in the application bootstrap, so the same instance will be used
|
|
||||||
* along the web application.
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class KeycloakService {
|
|
||||||
/**
|
|
||||||
* Keycloak-js instance.
|
|
||||||
*/
|
|
||||||
private _instance: Keycloak.KeycloakInstance;
|
|
||||||
/**
|
|
||||||
* User profile as KeycloakProfile interface.
|
|
||||||
*/
|
|
||||||
private _userProfile: Keycloak.KeycloakProfile;
|
|
||||||
/**
|
|
||||||
* Flag to indicate if the bearer will not be added to the authorization header.
|
|
||||||
*/
|
|
||||||
private _enableBearerInterceptor: boolean;
|
|
||||||
/**
|
|
||||||
* When the implicit flow is choosen there must exist a silentRefresh, as there is
|
|
||||||
* no refresh token.
|
|
||||||
*/
|
|
||||||
private _silentRefresh: boolean;
|
|
||||||
/**
|
|
||||||
* Indicates that the user profile should be loaded at the keycloak initialization,
|
|
||||||
* just after the login.
|
|
||||||
*/
|
|
||||||
private _loadUserProfileAtStartUp: boolean;
|
|
||||||
/**
|
|
||||||
* The bearer prefix that will be appended to the Authorization Header.
|
|
||||||
*/
|
|
||||||
private _bearerPrefix: string;
|
|
||||||
/**
|
|
||||||
* Value that will be used as the Authorization Http Header name.
|
|
||||||
*/
|
|
||||||
private _authorizationHeaderName: string;
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* The excluded urls patterns that must skip the KeycloakBearerInterceptor.
|
|
||||||
*/
|
|
||||||
private _excludedUrls: ExcludedUrlRegex[];
|
|
||||||
/**
|
|
||||||
* Observer for the keycloak events
|
|
||||||
*/
|
|
||||||
private _keycloakEvents$: Subject<KeycloakEvent> = new Subject<KeycloakEvent>();
|
|
||||||
/**
|
|
||||||
* The amount of required time remaining before expiry of the token before the token will be refreshed.
|
|
||||||
*/
|
|
||||||
private _updateMinValidity: number;
|
|
||||||
/**
|
|
||||||
* Returns true if the request should have the token added to the headers by the KeycloakBearerInterceptor.
|
|
||||||
*/
|
|
||||||
shouldAddToken: (request: HttpRequest<unknown>) => boolean;
|
|
||||||
/**
|
|
||||||
* Returns true if the request being made should potentially update the token.
|
|
||||||
*/
|
|
||||||
shouldUpdateToken: (request: HttpRequest<unknown>) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the keycloak-js events to the keycloakEvents Subject
|
|
||||||
* which is a good way to monitor for changes, if needed.
|
|
||||||
*
|
|
||||||
* The keycloakEvents returns the keycloak-js event type and any
|
|
||||||
* argument if the source function provides any.
|
|
||||||
*/
|
|
||||||
private bindsKeycloakEvents(): void {
|
|
||||||
this._instance.onAuthError = errorData => {
|
|
||||||
this._keycloakEvents$.next({
|
|
||||||
args: errorData,
|
|
||||||
type: KeycloakEventType.OnAuthError,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this._instance.onAuthLogout = () => {
|
|
||||||
this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthLogout });
|
|
||||||
};
|
|
||||||
|
|
||||||
this._instance.onAuthRefreshSuccess = () => {
|
|
||||||
this._keycloakEvents$.next({
|
|
||||||
type: KeycloakEventType.OnAuthRefreshSuccess,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this._instance.onAuthRefreshError = () => {
|
|
||||||
this._keycloakEvents$.next({
|
|
||||||
type: KeycloakEventType.OnAuthRefreshError,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this._instance.onAuthSuccess = () => {
|
|
||||||
this._keycloakEvents$.next({ type: KeycloakEventType.OnAuthSuccess });
|
|
||||||
};
|
|
||||||
|
|
||||||
this._instance.onTokenExpired = () => {
|
|
||||||
this._keycloakEvents$.next({
|
|
||||||
type: KeycloakEventType.OnTokenExpired,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this._instance.onActionUpdate = state => {
|
|
||||||
this._keycloakEvents$.next({
|
|
||||||
args: state,
|
|
||||||
type: KeycloakEventType.OnActionUpdate,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this._instance.onReady = authenticated => {
|
|
||||||
this._keycloakEvents$.next({
|
|
||||||
args: authenticated,
|
|
||||||
type: KeycloakEventType.OnReady,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all bearerExcludedUrl content in a uniform type: ExcludedUrl,
|
|
||||||
* so it becomes easier to handle.
|
|
||||||
*
|
|
||||||
* @param bearerExcludedUrls array of strings or ExcludedUrl that includes
|
|
||||||
* the url and HttpMethod.
|
|
||||||
*/
|
|
||||||
private loadExcludedUrls(bearerExcludedUrls: (string | ExcludedUrl)[]): ExcludedUrlRegex[] {
|
|
||||||
const excludedUrls: ExcludedUrlRegex[] = [];
|
|
||||||
for (const item of bearerExcludedUrls) {
|
|
||||||
let excludedUrl: ExcludedUrlRegex;
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
excludedUrl = { urlPattern: new RegExp(item, 'i'), httpMethods: [] };
|
|
||||||
} else {
|
|
||||||
excludedUrl = {
|
|
||||||
urlPattern: new RegExp(item.url, 'i'),
|
|
||||||
httpMethods: item.httpMethods,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
excludedUrls.push(excludedUrl);
|
|
||||||
}
|
|
||||||
return excludedUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the class values initialization.
|
|
||||||
*
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
private initServiceValues({
|
|
||||||
enableBearerInterceptor = true,
|
|
||||||
loadUserProfileAtStartUp = false,
|
|
||||||
bearerExcludedUrls = [],
|
|
||||||
authorizationHeaderName = 'Authorization',
|
|
||||||
bearerPrefix = 'Bearer',
|
|
||||||
initOptions,
|
|
||||||
updateMinValidity = 20,
|
|
||||||
shouldAddToken = () => true,
|
|
||||||
shouldUpdateToken = () => true,
|
|
||||||
}: KeycloakOptions): void {
|
|
||||||
this._enableBearerInterceptor = enableBearerInterceptor;
|
|
||||||
this._loadUserProfileAtStartUp = loadUserProfileAtStartUp;
|
|
||||||
this._authorizationHeaderName = authorizationHeaderName;
|
|
||||||
this._bearerPrefix = bearerPrefix.trim().concat(' ');
|
|
||||||
this._excludedUrls = this.loadExcludedUrls(bearerExcludedUrls);
|
|
||||||
this._silentRefresh = initOptions ? initOptions.flow === 'implicit' : false;
|
|
||||||
this._updateMinValidity = updateMinValidity;
|
|
||||||
this.shouldAddToken = shouldAddToken;
|
|
||||||
this.shouldUpdateToken = shouldUpdateToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keycloak initialization. It should be called to initialize the adapter.
|
|
||||||
* Options is an object with 2 main parameters: config and initOptions. The first one
|
|
||||||
* will be used to create the Keycloak instance. The second one are options to initialize the
|
|
||||||
* keycloak instance.
|
|
||||||
*
|
|
||||||
* @param options
|
|
||||||
* Config: may be a string representing the keycloak URI or an object with the
|
|
||||||
* following content:
|
|
||||||
* - url: Keycloak json URL
|
|
||||||
* - realm: realm name
|
|
||||||
* - clientId: client id
|
|
||||||
*
|
|
||||||
* initOptions:
|
|
||||||
* Options to initialize the Keycloak adapter, matches the options as provided by Keycloak itself.
|
|
||||||
*
|
|
||||||
* enableBearerInterceptor:
|
|
||||||
* Flag to indicate if the bearer will added to the authorization header.
|
|
||||||
*
|
|
||||||
* loadUserProfileInStartUp:
|
|
||||||
* Indicates that the user profile should be loaded at the keycloak initialization,
|
|
||||||
* just after the login.
|
|
||||||
*
|
|
||||||
* bearerExcludedUrls:
|
|
||||||
* String Array to exclude the urls that should not have the Authorization Header automatically
|
|
||||||
* added.
|
|
||||||
*
|
|
||||||
* authorizationHeaderName:
|
|
||||||
* This value will be used as the Authorization Http Header name.
|
|
||||||
*
|
|
||||||
* bearerPrefix:
|
|
||||||
* This value will be included in the Authorization Http Header param.
|
|
||||||
*
|
|
||||||
* tokenUpdateExcludedHeaders:
|
|
||||||
* Array of Http Header key/value maps that should not trigger the token to be updated.
|
|
||||||
*
|
|
||||||
* updateMinValidity:
|
|
||||||
* This value determines if the token will be refreshed based on its expiration time.
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
* A Promise with a boolean indicating if the initialization was successful.
|
|
||||||
*/
|
|
||||||
public async init(options: KeycloakOptions = {}) {
|
|
||||||
this.initServiceValues(options);
|
|
||||||
const { config, initOptions } = options;
|
|
||||||
|
|
||||||
this._instance = new Keycloak(config);
|
|
||||||
this.bindsKeycloakEvents();
|
|
||||||
|
|
||||||
const authenticated = await this._instance.init(initOptions);
|
|
||||||
|
|
||||||
if (authenticated && this._loadUserProfileAtStartUp) {
|
|
||||||
await this.loadUserProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
return authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to login form on (options is an optional object with redirectUri and/or
|
|
||||||
* prompt fields).
|
|
||||||
*
|
|
||||||
* @param options
|
|
||||||
* Object, where:
|
|
||||||
* - redirectUri: Specifies the uri to redirect to after login.
|
|
||||||
* - prompt:By default the login screen is displayed if the user is not logged-in to Keycloak.
|
|
||||||
* To only authenticate to the application if the user is already logged-in and not display the
|
|
||||||
* login page if the user is not logged-in, set this option to none. To always require
|
|
||||||
* re-authentication and ignore SSO, set this option to login .
|
|
||||||
* - maxAge: Used just if user is already authenticated. Specifies maximum time since the
|
|
||||||
* authentication of user happened. If user is already authenticated for longer time than
|
|
||||||
* maxAge, the SSO is ignored and he will need to re-authenticate again.
|
|
||||||
* - loginHint: Used to pre-fill the username/email field on the login form.
|
|
||||||
* - action: If value is 'register' then user is redirected to registration page, otherwise to
|
|
||||||
* login page.
|
|
||||||
* - locale: Specifies the desired locale for the UI.
|
|
||||||
* @returns
|
|
||||||
* A void Promise if the login is successful and after the user profile loading.
|
|
||||||
*/
|
|
||||||
public async login(options: Keycloak.KeycloakLoginOptions = {}) {
|
|
||||||
await this._instance.login(options);
|
|
||||||
|
|
||||||
if (this._loadUserProfileAtStartUp) {
|
|
||||||
await this.loadUserProfile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to logout.
|
|
||||||
*
|
|
||||||
* @param redirectUri
|
|
||||||
* Specifies the uri to redirect to after logout.
|
|
||||||
* @returns
|
|
||||||
* A void Promise if the logout was successful, cleaning also the userProfile.
|
|
||||||
*/
|
|
||||||
public async logout(redirectUri?: string) {
|
|
||||||
const options = {
|
|
||||||
redirectUri,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this._instance.logout(options);
|
|
||||||
this._userProfile = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to registration form. Shortcut for login with option
|
|
||||||
* action = 'register'. Options are same as for the login method but 'action' is set to
|
|
||||||
* 'register'.
|
|
||||||
*
|
|
||||||
* @param options
|
|
||||||
* login options
|
|
||||||
* @returns
|
|
||||||
* A void Promise if the register flow was successful.
|
|
||||||
*/
|
|
||||||
public async register(options: Keycloak.KeycloakLoginOptions = { action: 'register' }) {
|
|
||||||
await this._instance.register(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the user has access to the specified role. It will look for roles in
|
|
||||||
* realm and the given resource, but will not check if the user is logged in for better performance.
|
|
||||||
*
|
|
||||||
* @param role
|
|
||||||
* role name
|
|
||||||
* @param resource
|
|
||||||
* resource name. If not specified, `clientId` is used
|
|
||||||
* @returns
|
|
||||||
* A boolean meaning if the user has the specified Role.
|
|
||||||
*/
|
|
||||||
isUserInRole(role: string, resource?: string): boolean {
|
|
||||||
let hasRole: boolean;
|
|
||||||
hasRole = this._instance.hasResourceRole(role, resource);
|
|
||||||
if (!hasRole) {
|
|
||||||
hasRole = this._instance.hasRealmRole(role);
|
|
||||||
}
|
|
||||||
return hasRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the roles of the logged user. The realmRoles parameter, with default value
|
|
||||||
* true, will return the resource roles and realm roles associated with the logged user. If set to false
|
|
||||||
* it will only return the resource roles. The resource parameter, if specified, will return only resource roles
|
|
||||||
* associated with the given resource.
|
|
||||||
*
|
|
||||||
* @param realmRoles
|
|
||||||
* Set to false to exclude realm roles (only client roles)
|
|
||||||
* @param resource
|
|
||||||
* resource name If not specified, returns roles from all resources
|
|
||||||
* @returns
|
|
||||||
* Array of Roles associated with the logged user.
|
|
||||||
*/
|
|
||||||
getUserRoles(realmRoles: boolean = true, resource?: string): string[] {
|
|
||||||
let roles: string[] = [];
|
|
||||||
|
|
||||||
if (this._instance.resourceAccess) {
|
|
||||||
Object.keys(this._instance.resourceAccess).forEach(key => {
|
|
||||||
if (resource && resource !== key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceAccess = this._instance.resourceAccess[key];
|
|
||||||
const clientRoles = resourceAccess['roles'] || [];
|
|
||||||
roles = roles.concat(clientRoles);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (realmRoles && this._instance.realmAccess) {
|
|
||||||
const realmRoles = this._instance.realmAccess['roles'] || [];
|
|
||||||
roles.push(...realmRoles);
|
|
||||||
}
|
|
||||||
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user is logged in.
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
* A boolean that indicates if the user is logged in.
|
|
||||||
*/
|
|
||||||
isLoggedIn(): boolean {
|
|
||||||
if (!this._instance) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._instance.authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the token has less than minValidity seconds left before
|
|
||||||
* it expires.
|
|
||||||
*
|
|
||||||
* @param minValidity
|
|
||||||
* Seconds left. (minValidity) is optional. Default value is 0.
|
|
||||||
* @returns
|
|
||||||
* Boolean indicating if the token is expired.
|
|
||||||
*/
|
|
||||||
isTokenExpired(minValidity: number = 0): boolean {
|
|
||||||
return this._instance.isTokenExpired(minValidity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the token expires within _updateMinValidity seconds the token is refreshed. If the
|
|
||||||
* session status iframe is enabled, the session status is also checked.
|
|
||||||
* Returns a promise telling if the token was refreshed or not. If the session is not active
|
|
||||||
* anymore, the promise is rejected.
|
|
||||||
*
|
|
||||||
* @param minValidity
|
|
||||||
* Seconds left. (minValidity is optional, if not specified updateMinValidity - default 20 is used)
|
|
||||||
* @returns
|
|
||||||
* Promise with a boolean indicating if the token was succesfully updated.
|
|
||||||
*/
|
|
||||||
public async updateToken(minValidity = this._updateMinValidity) {
|
|
||||||
// TODO: this is a workaround until the silent refresh (issue #43)
|
|
||||||
// is not implemented, avoiding the redirect loop.
|
|
||||||
if (this._silentRefresh) {
|
|
||||||
if (this.isTokenExpired()) {
|
|
||||||
throw new Error('Failed to refresh the token, or the session is expired');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._instance) {
|
|
||||||
throw new Error('Keycloak Angular library is not initialized.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this._instance.updateToken(minValidity);
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the user profile.
|
|
||||||
* Returns promise to set functions to be invoked if the profile was loaded
|
|
||||||
* successfully, or if the profile could not be loaded.
|
|
||||||
*
|
|
||||||
* @param forceReload
|
|
||||||
* If true will force the loadUserProfile even if its already loaded.
|
|
||||||
* @returns
|
|
||||||
* A promise with the KeycloakProfile data loaded.
|
|
||||||
*/
|
|
||||||
public async loadUserProfile(forceReload = false) {
|
|
||||||
if (this._userProfile && !forceReload) {
|
|
||||||
return this._userProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._instance.authenticated) {
|
|
||||||
throw new Error('The user profile was not loaded as the user is not logged in.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (this._userProfile = await this._instance.loadUserProfile());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the authenticated token.
|
|
||||||
*/
|
|
||||||
public async getToken() {
|
|
||||||
return this._instance.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the logged username.
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
* The logged username.
|
|
||||||
*/
|
|
||||||
public getUsername() {
|
|
||||||
if (!this._userProfile) {
|
|
||||||
throw new Error('User not logged in or user profile was not loaded.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._userProfile.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear authentication state, including tokens. This can be useful if application
|
|
||||||
* has detected the session was expired, for example if updating token fails.
|
|
||||||
* Invoking this results in onAuthLogout callback listener being invoked.
|
|
||||||
*/
|
|
||||||
clearToken(): void {
|
|
||||||
this._instance.clearToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a valid token in header. The key & value format is:
|
|
||||||
* Authorization Bearer <token>.
|
|
||||||
* If the headers param is undefined it will create the Angular headers object.
|
|
||||||
*
|
|
||||||
* @param headers
|
|
||||||
* Updated header with Authorization and Keycloak token.
|
|
||||||
* @returns
|
|
||||||
* An observable with with the HTTP Authorization header and the current token.
|
|
||||||
*/
|
|
||||||
public addTokenToHeader(headers: HttpHeaders = new HttpHeaders()) {
|
|
||||||
return from(this.getToken()).pipe(map(token => (token ? headers.set(this._authorizationHeaderName, this._bearerPrefix + token) : headers)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the original Keycloak instance, if you need any customization that
|
|
||||||
* this Angular service does not support yet. Use with caution.
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
* The KeycloakInstance from keycloak-js.
|
|
||||||
*/
|
|
||||||
getKeycloakInstance(): Keycloak.KeycloakInstance {
|
|
||||||
return this._instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* Returns the excluded URLs that should not be considered by
|
|
||||||
* the http interceptor which automatically adds the authorization header in the Http Request.
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
* The excluded urls that must not be intercepted by the KeycloakBearerInterceptor.
|
|
||||||
*/
|
|
||||||
get excludedUrls(): ExcludedUrlRegex[] {
|
|
||||||
return this._excludedUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag to indicate if the bearer will be added to the authorization header.
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
* Returns if the bearer interceptor was set to be disabled.
|
|
||||||
*/
|
|
||||||
get enableBearerInterceptor(): boolean {
|
|
||||||
return this._enableBearerInterceptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keycloak subject to monitor the events triggered by keycloak-js.
|
|
||||||
* The following events as available (as described at keycloak docs -
|
|
||||||
* https://www.keycloak.org/docs/latest/securing_apps/index.html#callback-events):
|
|
||||||
* - OnAuthError
|
|
||||||
* - OnAuthLogout
|
|
||||||
* - OnAuthRefreshError
|
|
||||||
* - OnAuthRefreshSuccess
|
|
||||||
* - OnAuthSuccess
|
|
||||||
* - OnReady
|
|
||||||
* - OnTokenExpire
|
|
||||||
* In each occurrence of any of these, this subject will return the event type,
|
|
||||||
* described at {@link KeycloakEventType} enum and the function args from the keycloak-js
|
|
||||||
* if provided any.
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
* A subject with the {@link KeycloakEvent} which describes the event type and attaches the
|
|
||||||
* function args.
|
|
||||||
*/
|
|
||||||
get keycloakEvents$(): Subject<KeycloakEvent> {
|
|
||||||
return this._keycloakEvents$;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,8 +20,8 @@ export class ListingsService {
|
|||||||
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
|
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
getListingByUserId(userid: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
||||||
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${userid}`));
|
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
|
||||||
}
|
}
|
||||||
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
|
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
|
||||||
if (listing.id) {
|
if (listing.id) {
|
||||||
@@ -40,11 +40,4 @@ export class ListingsService {
|
|||||||
async deleteCommercialPropertyListing(id: string, imagePath: string) {
|
async deleteCommercialPropertyListing(id: string, imagePath: string) {
|
||||||
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/${id}/${imagePath}`));
|
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/${id}/${imagePath}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPropertyImages(id: string): Promise<string[]> {
|
|
||||||
return await lastValueFrom(this.http.get<string[]>(`${this.apiBaseUrl}/bizmatch/image/${id}`));
|
|
||||||
}
|
|
||||||
async changeImageOrder(id: string, propertyImages: string[]): Promise<string[]> {
|
|
||||||
return await lastValueFrom(this.http.put<string[]>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/imageOrder/${id}`, propertyImages));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { InitEditableRow } from 'primeng/table';
|
import { Injectable } from '@angular/core';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
|
import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -13,67 +12,58 @@ export class SelectOptionsService {
|
|||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const allSelectOptions = await lastValueFrom(
|
const allSelectOptions = await lastValueFrom(this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`));
|
||||||
this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`)
|
|
||||||
);
|
|
||||||
this.typesOfBusiness = allSelectOptions.typesOfBusiness;
|
this.typesOfBusiness = allSelectOptions.typesOfBusiness;
|
||||||
this.prices = allSelectOptions.prices;
|
this.prices = allSelectOptions.prices;
|
||||||
this.listingCategories = allSelectOptions.listingCategories;
|
this.listingCategories = allSelectOptions.listingCategories;
|
||||||
this.categories = allSelectOptions.categories;
|
this.customerTypes = allSelectOptions.customerTypes;
|
||||||
this.states = allSelectOptions.locations;
|
this.states = allSelectOptions.locations;
|
||||||
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty
|
this.gender = allSelectOptions.gender;
|
||||||
|
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty;
|
||||||
}
|
}
|
||||||
public typesOfBusiness: Array<KeyValueStyle>;
|
public typesOfBusiness: Array<KeyValueStyle>;
|
||||||
|
|
||||||
public typesOfCommercialProperty: Array<KeyValueStyle>;
|
public typesOfCommercialProperty: Array<KeyValueStyle>;
|
||||||
|
|
||||||
public prices: Array<KeyValue>;
|
public prices: Array<KeyValue>;
|
||||||
|
|
||||||
public listingCategories: Array<KeyValue>;
|
public listingCategories: Array<KeyValue>;
|
||||||
|
|
||||||
public categories: Array<KeyValueStyle>;
|
public customerTypes: Array<KeyValue>;
|
||||||
|
|
||||||
|
public gender: Array<KeyValue>;
|
||||||
|
|
||||||
public states: Array<any>;
|
public states: Array<any>;
|
||||||
|
|
||||||
getState(value:string):string{
|
getState(value: string): string {
|
||||||
return this.states.find(l=>l.value===value)?.name
|
return this.states.find(l => l.value === value)?.name;
|
||||||
}
|
}
|
||||||
getBusiness(value:number):string{
|
getBusiness(value: number): string {
|
||||||
return this.typesOfBusiness.find(t=>t.value===String(value))?.name
|
return this.typesOfBusiness.find(t => t.value === String(value))?.name;
|
||||||
}
|
}
|
||||||
getCommercialProperty(value:number):string{
|
getCommercialProperty(value: number): string {
|
||||||
return this.typesOfCommercialProperty.find(t=>t.value===String(value))?.name
|
return this.typesOfCommercialProperty.find(t => t.value === String(value))?.name;
|
||||||
}
|
}
|
||||||
getListingsCategory(value:string):string{
|
getListingsCategory(value: string): string {
|
||||||
return this.listingCategories.find(l=>l.value===value)?.name
|
return this.listingCategories.find(l => l.value === value)?.name;
|
||||||
}
|
}
|
||||||
getCategory(value:string):string{
|
getCustomerType(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.name
|
return this.customerTypes.find(c => c.value === value)?.name;
|
||||||
}
|
}
|
||||||
getIcon(value:string):string{
|
getGender(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.icon
|
return this.gender.find(c => c.value === value)?.name;
|
||||||
}
|
}
|
||||||
getTextColor(value:string):string{
|
getIconType(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.textColorClass
|
return this.typesOfBusiness.find(c => c.value === value)?.icon;
|
||||||
}
|
}
|
||||||
getBgColor(value:string):string{
|
getTextColorType(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.bgColorClass
|
return this.typesOfBusiness.find(c => c.value === value)?.textColorClass;
|
||||||
}
|
}
|
||||||
getIconAndTextColor(value:string):string{
|
getBgColorType(value: number): string {
|
||||||
const category = this.categories.find(c=>c.value===value)
|
return this.typesOfBusiness.find(c => c.value === String(value))?.bgColorClass;
|
||||||
return `${category?.icon} ${category?.textColorClass}`
|
|
||||||
}
|
}
|
||||||
getIconType(value:string):string{
|
getIconAndTextColorType(value: number): string {
|
||||||
return this.typesOfBusiness.find(c=>c.value===value)?.icon
|
const category = this.typesOfBusiness.find(c => c.value === String(value));
|
||||||
}
|
return `${category?.icon} ${category?.textColorClass}`;
|
||||||
getTextColorType(value:string):string{
|
|
||||||
return this.typesOfBusiness.find(c=>c.value===value)?.textColorClass
|
|
||||||
}
|
|
||||||
getBgColorType(value:number):string{
|
|
||||||
return this.typesOfBusiness.find(c=>c.value===String(value))?.bgColorClass
|
|
||||||
}
|
|
||||||
getIconAndTextColorType(value:number):string{
|
|
||||||
const category = this.typesOfBusiness.find(c=>c.value===String(value))
|
|
||||||
return `${category?.icon} ${category?.textColorClass}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export class UserService {
|
|||||||
async getAllStates(): Promise<any> {
|
async getAllStates(): Promise<any> {
|
||||||
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
|
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
|
||||||
}
|
}
|
||||||
async getId(email: string): Promise<string> {
|
// async getId(email: string): Promise<string> {
|
||||||
if (sessionStorage.getItem('USERID')) {
|
// if (sessionStorage.getItem('USERID')) {
|
||||||
return sessionStorage.getItem('USERID');
|
// return sessionStorage.getItem('USERID');
|
||||||
} else {
|
// } else {
|
||||||
const user = await this.getByMail(email);
|
// const user = await this.getByMail(email);
|
||||||
sessionStorage.setItem('USERID', user.id);
|
// sessionStorage.setItem('USERID', user.id);
|
||||||
return user.id;
|
// return user.id;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,14 @@ export function createLogger(name: string, level: number = INFO, options: any =
|
|||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export function formatPhoneNumber(phone: string): string {
|
||||||
|
const cleaned = ('' + phone).replace(/\D/g, '');
|
||||||
|
const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
|
||||||
|
if (match) {
|
||||||
|
return '(' + match[1] + ') ' + match[2] + '-' + match[3];
|
||||||
|
}
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
export const getSessionStorageHandler = function (path, value, previous, applyData) {
|
export const getSessionStorageHandler = function (path, value, previous, applyData) {
|
||||||
sessionStorage.setItem('criteria', JSON.stringify(this));
|
sessionStorage.setItem('criteria', JSON.stringify(this));
|
||||||
};
|
};
|
||||||
@@ -130,3 +137,25 @@ export function map2User(jwt: string): KeycloakUser {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function getImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
resolve({ width: img.width, height: img.height });
|
||||||
|
};
|
||||||
|
img.src = imageUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDialogWidth(dimensions): string {
|
||||||
|
const aspectRatio = dimensions.width / dimensions.height;
|
||||||
|
let dialogWidth = '50vw'; // Standardbreite
|
||||||
|
|
||||||
|
// Passen Sie die Breite basierend auf dem Seitenverhältnis an
|
||||||
|
if (aspectRatio < 1) {
|
||||||
|
dialogWidth = '30vw'; // Hochformat
|
||||||
|
} else if (aspectRatio > 1) {
|
||||||
|
dialogWidth = '50vw'; // Querformat
|
||||||
|
}
|
||||||
|
return dialogWidth;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ export const environment_base = {
|
|||||||
url: 'https://auth.bizmatch.net',
|
url: 'https://auth.bizmatch.net',
|
||||||
realm: 'bizmatch-dev',
|
realm: 'bizmatch-dev',
|
||||||
clientId: 'bizmatch-dev',
|
clientId: 'bizmatch-dev',
|
||||||
|
redirectUri: 'https://dev.bizmatch.net',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ environment.mailinfoUrl = 'http://localhost:4200';
|
|||||||
environment.imageBaseUrl = 'http://localhost:4200';
|
environment.imageBaseUrl = 'http://localhost:4200';
|
||||||
environment.keycloak.clientId = 'dev';
|
environment.keycloak.clientId = 'dev';
|
||||||
environment.keycloak.realm = 'dev';
|
environment.keycloak.realm = 'dev';
|
||||||
|
environment.keycloak.redirectUri = 'http://localhost:4200';
|
||||||
|
|||||||
86
bizmatch/src/keycloak.ts
Normal file
86
bizmatch/src/keycloak.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { KeycloakAdapter, KeycloakInstance, KeycloakLoginOptions, KeycloakLogoutOptions, KeycloakRegisterOptions } from 'keycloak-js';
|
||||||
|
import { createLogger } from './app/utils/utils';
|
||||||
|
|
||||||
|
const logger = createLogger('keycloak');
|
||||||
|
export type OptionsOrProvider<T> = Partial<T> | (() => Partial<T>);
|
||||||
|
/**
|
||||||
|
* Create and immediately resolve a KeycloakPromise
|
||||||
|
*/
|
||||||
|
const createPromise = () => new Promise<void>(resolve => resolve());
|
||||||
|
/**
|
||||||
|
* Resolve OptionsOrProvider: if it's an function, execute it, otherwise return it.
|
||||||
|
*/
|
||||||
|
const resolveOptions = <T>(opt: OptionsOrProvider<T>): Partial<T> => (typeof opt === 'function' ? opt() : opt);
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Update options with the overrides given as OptionsOrProvider
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @param overrides
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const updateOptions = <T>(options: T, overrides: OptionsOrProvider<T>): T => Object.assign(options ?? <T>{}, resolveOptions(overrides));
|
||||||
|
/**
|
||||||
|
* Keycloak adapter that supports options customization.
|
||||||
|
*
|
||||||
|
* All options can either be given as lazily evaluated provider functions (that will be evaluated
|
||||||
|
* right before navigating) or eagerly evaluated objects. These options will have precedence
|
||||||
|
* over options passed by keycloak-js.
|
||||||
|
*
|
||||||
|
* Cf. https://www.keycloak.org/docs/15.0/securing_apps/#custom-adapters
|
||||||
|
*
|
||||||
|
* Actual implementation copied more or less verbatim from
|
||||||
|
* https://github.com/keycloak/keycloak-js-bower/blob/10.0.2/dist/keycloak.js#L1136
|
||||||
|
*
|
||||||
|
* @param kc Function that returns a Keycloak instance
|
||||||
|
* @param loginOptions login options
|
||||||
|
* @param logoutOptions logout options
|
||||||
|
* @param registerOptions register options
|
||||||
|
* @returns KeycloakAdapter
|
||||||
|
*/
|
||||||
|
export function customKeycloakAdapter(
|
||||||
|
kc: () => KeycloakInstance,
|
||||||
|
loginOptions: OptionsOrProvider<KeycloakLoginOptions> = {},
|
||||||
|
logoutOptions: OptionsOrProvider<KeycloakLogoutOptions> = {},
|
||||||
|
registerOptions: OptionsOrProvider<KeycloakRegisterOptions> = {},
|
||||||
|
): KeycloakAdapter {
|
||||||
|
return {
|
||||||
|
login: (options?: KeycloakLoginOptions): Promise<void> => {
|
||||||
|
updateOptions(options, loginOptions);
|
||||||
|
logger.info('Executing login. Options: ', options);
|
||||||
|
window.location.replace(kc().createLoginUrl(options));
|
||||||
|
return createPromise();
|
||||||
|
},
|
||||||
|
logout: (options?: KeycloakLogoutOptions): Promise<void> => {
|
||||||
|
updateOptions(options, logoutOptions);
|
||||||
|
logger.info('Executing logout. Options: ', options);
|
||||||
|
window.location.replace(kc().createLogoutUrl(options));
|
||||||
|
return createPromise();
|
||||||
|
},
|
||||||
|
register: (options?: KeycloakRegisterOptions): Promise<void> => {
|
||||||
|
updateOptions(options, registerOptions);
|
||||||
|
logger.info('Executing register. Options: ', options);
|
||||||
|
window.location.replace(kc().createRegisterUrl(options));
|
||||||
|
return createPromise();
|
||||||
|
},
|
||||||
|
accountManagement: (): Promise<void> => {
|
||||||
|
const accountUrl = kc().createAccountUrl();
|
||||||
|
logger.info('Executing account management');
|
||||||
|
if (typeof accountUrl !== 'undefined') {
|
||||||
|
window.location.href = accountUrl;
|
||||||
|
} else {
|
||||||
|
throw new Error('Not supported by the OIDC server');
|
||||||
|
}
|
||||||
|
return createPromise();
|
||||||
|
},
|
||||||
|
redirectUri: (options: { redirectUri: string }) => {
|
||||||
|
if (options?.redirectUri) {
|
||||||
|
return options.redirectUri;
|
||||||
|
}
|
||||||
|
if (kc().redirectUri) {
|
||||||
|
return kc().redirectUri;
|
||||||
|
}
|
||||||
|
return window.location.href;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user