46 Commits

Author SHA1 Message Date
3d5b7e3f39 account changes 2024-06-03 09:35:42 -05:00
d488f90f48 BugFix: #38 EMail Us 2024-05-29 17:03:23 -05:00
044f8efa0f Bug Fixed: #34,#28,#48,#46,#2,#15 2024-05-29 12:41:55 -05:00
24a3d210f0 renew ts value, better logging JWT 2024-05-28 17:26:45 -05:00
2465b8966b add more logging 2024-05-28 14:41:26 -05:00
e87222d3c1 findByImagePath: do not check for draft because it's internally ... 2024-05-28 14:19:32 -05:00
902ab9caed acc. draft mode, take care of ADMIN role or on own listings 2024-05-28 13:15:31 -05:00
44acbcd4d0 safe if user===undefined 2024-05-28 11:55:26 -05:00
b4cf17b8ea Draft Mode inkl. Token implementiert 2024-05-28 11:30:00 -05:00
226d2ebc1e Auth Token Übersendung eingebaut 2024-05-27 18:02:47 -05:00
0473f74241 Issues #47,#42, #35 resolved, request logging erweitert 2024-05-24 18:15:33 -05:00
c9d94e973a Fix for Issue #45 2024-05-24 10:52:11 -05:00
f9d9c6ad9e image path adjusted 2024-05-23 18:39:25 -05:00
5dc893da38 imagePath changed 2024-05-23 18:09:54 -05:00
c471629c6d add serialId 2024-05-22 17:36:49 -05:00
13fb3cd4b8 Schema changed for commercials 2024-05-22 17:34:50 -05:00
d6768b3da9 waiting for initialization 2024-05-22 13:32:17 -05:00
7fdc87fb0b flow: 'implicit' 2024-05-22 11:40:49 -05:00
0b7e33612a new authorization approach 2024-05-22 11:05:40 -05:00
8fba3aa832 authGuard acc. lejdiprifti.com 2024-05-22 09:31:31 -05:00
214327031c Bug Fixes 2024-05-20 17:52:05 -05:00
dc9adb151d new initialization process, keycloak update 24.0.4 2024-05-20 15:54:01 -05:00
747435bfba first validation 2024-05-17 16:46:17 -05:00
782c254a33 --output-hashing=all added to build 2024-05-17 15:33:19 -05:00
df4e2b00e2 Issue fixing + deletion of profile & logo 2024-05-17 14:50:50 -05:00
0684b9534f Kriterium name reseted 2024-05-16 16:45:33 -05:00
e0ecea5af2 fix further bugs 2024-05-16 15:57:39 -05:00
327aef0f21 add version.js 2024-05-16 13:32:42 -05:00
cb73daf863 Version Info 2024-05-16 13:20:42 -05:00
08c53e2eb2 raw template 2024-05-16 12:14:56 -05:00
492c03c2be BugFix deletion of prop images 2024-05-16 10:08:23 -05:00
f51a298227 Bugfixes 2024-05-15 17:35:04 -05:00
474d7c63d5 timestamp based images 2024-05-14 15:28:08 -05:00
d2e5562602 history service, mail template improved, general listing entry 2024-05-14 11:53:20 -05:00
aff55c5433 BugFixes image upload, image display, new DB structure for areasServed, licenedIn 2024-05-13 17:31:01 -05:00
5230ef1230 images based on http-server, filter dropdowns 2024-05-10 17:19:36 +02:00
d508415de4 show all listings, Bug Fixes 2024-05-09 16:10:01 +02:00
6b61c19bd7 Bug Fixing overall 2024-05-06 20:13:09 +02:00
bb5a408cdc cleanup + Property images 2024-05-05 15:30:10 +02:00
9121ca1a69 Marketing 2024-04-24 17:10:44 +02:00
4230867608 Rework of major pages 2024-04-24 14:31:32 +02:00
9e03620be7 format on save, resolve compile errors, functionality 1. stage 2024-04-23 17:32:21 +02:00
7f0f21b598 Umbau auf postgres 2. step 2024-04-22 22:26:44 +02:00
c90d6b72b7 Komplettumstieg auf drizzle 2024-04-20 20:48:18 +02:00
c4cdcf4505 Umstellung postgres 2. part 2024-04-15 22:05:20 +02:00
7d10080069 Start Umbau zu postgres 2024-04-14 22:52:19 +02:00
172 changed files with 49642 additions and 6209 deletions

4
.gitignore vendored
View File

@@ -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
@@ -62,7 +65,6 @@ 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
*.js
*.map *.map
package-lock.json package-lock.json

View File

@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

View File

@@ -0,0 +1,45 @@
{
"env": {
"es2021": true,
"browser": true
},
"extends": [
"airbnb-base",
"airbnb-typescript",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier",
"plugin:cypress/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"project": ["./tsconfig.json"]
},
"plugins": ["@typescript-eslint"],
"rules": {
"import/no-unresolved": ["off"],
"import/prefer-default-export": ["off"],
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": ["error"],
"@typescript-eslint/lines-between-class-members": ["off"],
"no-param-reassign": ["off"],
"max-classes-per-file": ["off"],
"no-shadow": ["off"],
"class-methods-use-this": ["off"],
"react/jsx-filename-extension": ["off"],
"import/no-cycle": ["off"],
"radix": ["off"],
"no-promise-executor-return": ["off"],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "enumMember",
"format": ["UPPER_CASE", "PascalCase"]
}
],
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
"spaced-comment": ["off"],
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
}
}

View File

@@ -56,3 +56,4 @@ pids
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pictures pictures
pictures_base

View File

@@ -1,4 +1,18 @@
{ {
"arrowParens": "avoid",
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 220,
"proseWrap": "always",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
} }

View File

@@ -6,17 +6,46 @@
"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",
"request": "launch",
"name": "Debug Current TS File",
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"sourceMaps": true,
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "launch",
"name": "generateDefs",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
"sourceMaps": true,
"smartStep": true
},
{
"type": "node",
"request": "launch",
"name": "generateTypes",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
"sourceMaps": true,
"smartStep": true
} }
] ]
} }

28
bizmatch-server/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"editor.suggestSelection": "first",
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
"explorer.confirmDelete": false,
"typescript.updateImportsOnFileMove.enabled": "always",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"prettier.printWidth": 240,
"git.autofetch": false,
"git.autorefresh": true
}

View File

@@ -1 +0,0 @@
FT.CREATE listingsIndex ON JSON PREFIX 1 listings: SCHEMA $.location AS location TAG SORTABLE $.price AS price NUMERIC SORTABLE $.listingsCategory AS listingsCategory TAG SORTABLE $.type AS type TAG SORTABLE

1326
bizmatch-server/broker.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: "./src/drizzle/schema.ts",
out: "./src/drizzle/migrations",
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL,
},
verbose: true,
strict: true,
})

View File

@@ -4,7 +4,10 @@
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true, "deleteOutDir": true,
"assets": ["assets/**/*","**/*.hbs"], "assets": [
"assets/**/*",
"**/*.hbs"
],
"watchAssets": true "watchAssets": true
} }
} }

View File

@@ -9,16 +9,21 @@
"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",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"generate": "drizzle-kit generate:pg",
"drop": "drizzle-kit drop",
"migrate": "tsx src/drizzle/migrate.ts",
"import": "tsx src/drizzle/import.ts",
"generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^1.10.3", "@nestjs-modules/mailer": "^1.10.3",
@@ -29,7 +34,12 @@
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.1", "@nestjs/serve-static": "^4.0.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"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",
@@ -38,15 +48,19 @@
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.11.5",
"redis": "^4.6.13", "redis": "^4.6.13",
"redis-om": "^0.4.3", "redis-om": "^0.4.3",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sharp": "^0.33.2", "sharp": "^0.33.2",
"tsx": "^4.7.2",
"urlcat": "^3.1.0", "urlcat": "^3.1.0",
"winston": "^3.11.0" "winston": "^3.11.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/parser": "^7.24.4",
"@babel/traverse": "^7.24.1",
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
@@ -58,14 +72,20 @@
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.11.5",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"commander": "^12.0.0",
"drizzle-kit": "^0.20.16",
"eslint": "^8.42.0", "eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"kysely-codegen": "^0.15.0",
"pg-to-ts": "^4.1.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",

View File

@@ -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';
} }
} }

View File

@@ -1,36 +1,58 @@
import { Module } from '@nestjs/common'; import { MiddlewareConsumer, Module } from '@nestjs/common';
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 path from 'path';
import { fileURLToPath } from 'url';
import * as winston from 'winston';
import { AppController } from './app.controller.js'; import { AppController } from './app.controller.js';
import { AppService } from './app.service.js'; import { AppService } from './app.service.js';
import { FileService } from './file/file.service.js';
import { AuthService } from './auth/auth.service.js';
import { AuthController } from './auth/auth.controller.js';
import { ConfigModule } from '@nestjs/config';
import { SelectOptionsController } from './select-options/select-options.controller.js';
import { SelectOptionsService } from './select-options/select-options.service.js';
import { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
import { RedisModule } from './redis/redis.module.js';
import { ListingsService } from './listings/listings.service.js';
import { ServeStaticModule } from '@nestjs/serve-static';
import path, { join } from 'path';
import { fileURLToPath } from 'url';
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import { MailModule } from './mail/mail.module.js';
import { AuthModule } from './auth/auth.module.js'; import { AuthModule } from './auth/auth.module.js';
import { FileService } from './file/file.service.js';
import { GeoModule } from './geo/geo.module.js'; import { GeoModule } from './geo/geo.module.js';
import { UserModule } from './user/user.module.js';
import { ListingsModule } from './listings/listings.module.js';
import { SelectOptionsModule } from './select-options/select-options.module.js';
import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js';
import { ImageModule } from './image/image.module.js'; import { ImageModule } from './image/image.module.js';
import { ListingsModule } from './listings/listings.module.js';
import { MailModule } from './mail/mail.module.js';
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
import { SelectOptionsModule } from './select-options/select-options.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: [ConfigModule.forRoot(), MailModule, AuthModule, imports: [
ServeStaticModule.forRoot({ ConfigModule.forRoot({ isGlobal: true }),
rootPath: join(__dirname, '..', 'pictures'), // `public` ist das Verzeichnis, wo Ihre statischen Dateien liegen MailModule,
}), AuthModule,
WinstonModule.forRoot({ WinstonModule.forRoot({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
@@ -51,10 +73,14 @@ const __dirname = path.dirname(__filename);
UserModule, UserModule,
ListingsModule, ListingsModule,
SelectOptionsModule, SelectOptionsModule,
RedisModule, ImageModule,
ImageModule PassportModule,
], ],
controllers: [AppController, SubscriptionsController], controllers: [AppController],
providers: [AppService, FileService], providers: [AppService, FileService],
}) })
export class AppModule {} export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestDurationMiddleware).forRoutes('*');
}
}

View File

@@ -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 {}

View File

@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { drizzle } from 'drizzle-orm/node-postgres';
import pkg from 'pg';
const { Pool } = pkg;
import * as schema from './schema.js';
import { ConfigService } from '@nestjs/config';
import { jsonb, varchar } from 'drizzle-orm/pg-core';
import { PG_CONNECTION } from './schema.js';
@Module({
providers: [
{
provide: PG_CONNECTION,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const connectionString = configService.get<string>('DATABASE_URL');
const pool = new Pool({
connectionString,
// ssl: true,
});
return drizzle(pool, { schema, logger:true });
},
},
],
exports: [PG_CONNECTION],
})
export class DrizzleModule {}

View File

@@ -0,0 +1,143 @@
import fs from 'fs';
import ts from 'typescript';
export const TABLE_BO_MAPPING = {
'users':'User',
'businesses':'BusinessListing',
'commercials':'CommercialPropertyListing'
}
function generateInterfaceDefinitions(inputFile: string, outputFile: string): void {
const sourceFile = ts.createSourceFile(
inputFile,
fs.readFileSync(inputFile, 'utf8'),
ts.ScriptTarget.Latest,
true
);
let interfaceDefinitions = '';
ts.forEachChild(sourceFile, (node) => {
if (ts.isVariableStatement(node)) {
const variableDeclaration = node.declarationList.declarations[0];
if (variableDeclaration.initializer && ts.isCallExpression(variableDeclaration.initializer)) {
const initializer = variableDeclaration.initializer;
if (initializer.expression.getText(sourceFile) === 'pgTable') {
const tableName = initializer.arguments[0].getText(sourceFile).slice(1, -1); // Entfernen von zusätzlichen Anführungszeichen
const tableDefinition = initializer.arguments[1];
const interfaceName = TABLE_BO_MAPPING[tableName] ? TABLE_BO_MAPPING[tableName] : tableName.charAt(0).toUpperCase() + tableName.slice(1);
let interfaceDefinition = `export interface ${interfaceName} {\n`;
ts.forEachChild(tableDefinition, (propertyNode) => {
if (ts.isPropertyAssignment(propertyNode)) {
const propertyName = propertyNode.name.getText(sourceFile);
const propertyDefinition = propertyNode.initializer;
if (ts.isCallExpression(propertyDefinition)) {
const propertyType = getPropertyType(propertyDefinition, sourceFile);
const isOptional = !hasNonOptionalModifier(propertyDefinition.expression);
interfaceDefinition += ` ${propertyName}${isOptional ? '?' : ''}: ${propertyType};\n`;
}
}
});
interfaceDefinition += '}\n\n';
interfaceDefinitions += interfaceDefinition;
}
}
}
});
fs.writeFileSync(outputFile, interfaceDefinitions);
console.log(`Interface definitions generated successfully. Output file: ${outputFile}`);
}
function getPropertyType(propertyDefinition: ts.CallExpression, sourceFile: ts.SourceFile): string {
const typeFunction = getTypeFunctionName(propertyDefinition.expression, sourceFile);
let propertyType = '';
switch (typeFunction) {
case 'varchar':
case 'char':
case 'text':
case 'uuid':
propertyType = 'string';
break;
case 'integer':
case 'numeric':
case 'real':
case 'doublePrecision':
propertyType = 'number';
break;
case 'boolean':
propertyType = 'boolean';
break;
case 'timestamp':
propertyType = 'Date';
break;
default:
propertyType = 'any';
}
const isArray = hasArrayModifier(propertyDefinition.expression);
return isArray ? `${propertyType}[]` : propertyType;
}
function getTypeFunctionName(expression: ts.Expression, sourceFile: ts.SourceFile): string {
// Prüfen, ob die aktuelle Expression ein Identifier ist (SyntaxKind.Identifier hat den Wert 80)
if (expression.kind === ts.SyntaxKind.Identifier) {
return expression.getText(sourceFile);
}
// Wenn die Expression eine CallExpression oder eine PropertyAccessExpression ist,
// gehe zur nächsten Expression-Ebene
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
return getTypeFunctionName(expression.expression, sourceFile);
}
// Falls ein nicht unterstützter Expression-Typ vorliegt, gibt 'unknown' zurück
return 'unknown';
}
function hasArrayModifier(expression: ts.Expression): boolean {
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'array' ist
if (ts.isPropertyAccessExpression(expression) && expression.name.getText() === 'array') {
return true;
}
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
// prüfe rekursiv die nächste Ebene
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
return hasArrayModifier(expression.expression);
}
// Wenn keine weitere Ebene oder kein Array-Modifier gefunden wurde, gib false zurück
return false;
}
function hasNonOptionalModifier(expression: ts.Expression): boolean {
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'notNull' ist
if (ts.isPropertyAccessExpression(expression) && (expression.name.getText() === 'notNull' || expression.name.getText() === 'primaryKey')) {
return true;
}
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
// prüfe rekursiv die nächste Ebene
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
return hasNonOptionalModifier(expression.expression);
}
// Wenn keine weitere Ebene oder kein NotNull-Modifier gefunden wurde, gib false zurück
return false;
}
// Hauptprogramm
const inputFile = process.argv[2]|| './src/drizzle/schema.ts';
const outputFile = process.argv[3] || './model.ts';
if (!inputFile) {
console.error('Please provide an input file.');
process.exit(1);
}
generateInterfaceDefinitions(inputFile, outputFile);

View File

@@ -0,0 +1,194 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
import fs from 'fs-extra';
import { join } from 'path';
import pkg from 'pg';
import { rimraf } from 'rimraf';
import sharp from 'sharp';
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';
const { Pool } = pkg;
const connectionString = process.env.DATABASE_URL;
// const pool = new Pool({connectionString})
const client = new Pool({ connectionString });
const db = drizzle(client, { schema, logger: true });
//Delete Content
await db.delete(schema.commercials);
await db.delete(schema.businesses);
await db.delete(schema.users);
//Broker
let filePath = `./data/broker.json`;
let data: string = readFileSync(filePath, 'utf8');
const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten
const generatedUserData = [];
console.log(usersData.length);
let i = 0,
male = 0,
female = 0;
const targetPathProfile = `./pictures/profile`;
deleteFilesOfDir(targetPathProfile);
const targetPathLogo = `./pictures/logo`;
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) {
const user: User = { firstname: '', lastname: '', email: '' };
user.licensedIn = [];
userData.licensedIn.forEach(l => {
console.log(l['value'], l['name']);
user.licensedIn.push({ registerNo: l['value'], state: l['name'] });
});
user.areasServed = [];
user.areasServed = userData.areasServed.map(l => {
return { county: l.split(',')[0].trim(), state: l.split(',')[1].trim() };
});
user.hasCompanyLogo = true;
user.hasProfile = true;
user.firstname = userData.firstname;
user.lastname = userData.lastname;
user.email = userData.email;
user.phoneNumber = userData.phoneNumber;
user.description = userData.description;
user.companyName = userData.companyName;
user.companyOverview = userData.companyOverview;
user.companyWebsite = userData.companyWebsite;
user.companyLocation = userData.companyLocation;
user.offeredServices = userData.offeredServices;
user.gender = userData.gender;
user.created = new Date();
user.updated = new Date();
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]);
i++;
if (u[0].gender === 'male') {
male++;
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
await storeProfilePicture(data, emailToDirName(u[0].email));
} else {
female++;
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
await storeProfilePicture(data, emailToDirName(u[0].email));
}
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
await storeCompanyLogo(data, emailToDirName(u[0].email));
}
//Business Listings
filePath = `./data/businesses.json`;
data = readFileSync(filePath, 'utf8');
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
for (const business of businessJsonData) {
delete business.id;
business.created = new Date(business.created);
business.updated = new Date(business.created);
const user = getRandomItem(generatedUserData);
business.userId = user.insertedId;
business.imageName = emailToDirName(user.email);
await db.insert(schema.businesses).values(business);
}
//Corporate Listings
filePath = `./data/commercials.json`;
data = readFileSync(filePath, 'utf8');
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
for (const commercial of commercialJsonData) {
const id = commercial.id;
delete commercial.id;
const user = getRandomItem(generatedUserData);
commercial.imageOrder = getFilenames(id);
commercial.imagePath = emailToDirName(user.email);
const insertionDate = getRandomDateWithinLastYear();
commercial.created = insertionDate;
commercial.updated = insertionDate;
commercial.userId = user.insertedId;
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
await client.end();
function getRandomItem<T>(arr: T[]): T {
if (arr.length === 0) {
throw new Error('The array is empty.');
}
const randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex];
}
function getFilenames(id: string): string[] {
try {
let filePath = `./pictures_base/property/${id}`;
return readdirSync(filePath);
} catch (e) {
return null;
}
}
function getRandomDateWithinLastYear(): Date {
const currentDate = new Date();
const lastYear = new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), currentDate.getDate());
const timeDiff = currentDate.getTime() - lastYear.getTime();
const randomTimeDiff = Math.random() * timeDiff;
const randomDate = new Date(lastYear.getTime() + randomTimeDiff);
return randomDate;
}
async function storeProfilePicture(buffer: Buffer, userId: string) {
let quality = 50;
const output = await sharp(buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
}
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
let quality = 50;
const output = await sharp(buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
}
function deleteFilesOfDir(directoryPath) {
// Überprüfen, ob das Verzeichnis existiert
if (existsSync(directoryPath)) {
// Den Inhalt des Verzeichnisses synchron löschen
try {
readdirSync(directoryPath).forEach(file => {
const filePath = join(directoryPath, file);
// Wenn es sich um ein Verzeichnis handelt, rekursiv löschen
if (statSync(filePath).isDirectory()) {
rimraf.sync(filePath);
} else {
// Wenn es sich um eine Datei handelt, direkt löschen
unlinkSync(filePath);
}
});
console.log('Der Inhalt des Verzeichnisses wurde erfolgreich gelöscht.');
} catch (err) {
console.error('Fehler beim Löschen des Verzeichnisses:', err);
}
} else {
console.log('Das Verzeichnis existiert nicht.');
}
}

View File

@@ -0,0 +1,13 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import pkg from 'pg';
const { Pool } = pkg;
import * as schema from './schema.js';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
const connectionString = process.env.DATABASE_URL
const pool = new Pool({connectionString})
const db = drizzle(pool, { schema });
// This will run migrations on the database, skipping the ones already applied
await migrate(db, { migrationsFolder: './src/drizzle/migrations' });
// Don't forget to close the connection, otherwise the script will hang
await pool.end();

View File

@@ -0,0 +1,96 @@
DO $$ BEGIN
CREATE TYPE "gender" AS ENUM('male', 'female');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "businesses" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"userId" uuid,
"type" integer,
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"draft" boolean,
"listingsCategory" varchar(255),
"realEstateIncluded" boolean,
"leasedLocation" boolean,
"franchiseResale" boolean,
"salesRevenue" double precision,
"cashFlow" double precision,
"supportAndTraining" text,
"employees" integer,
"established" integer,
"internalListingNumber" integer,
"reasonForSale" varchar(255),
"brokerLicencing" varchar(255),
"internals" text,
"imagePath" varchar(200),
"created" timestamp,
"updated" timestamp,
"visits" integer,
"lastVisit" timestamp
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"serial_id" serial NOT NULL,
"userId" uuid,
"type" integer,
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"listingsCategory" varchar(255),
"hideImage" boolean,
"draft" boolean,
"zipCode" integer,
"county" varchar(255),
"email" varchar(255),
"website" varchar(255),
"phoneNumber" varchar(255),
"imageOrder" varchar(200)[],
"imagePath" varchar(200),
"created" timestamp,
"updated" timestamp,
"visits" integer,
"lastVisit" timestamp
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"firstname" varchar(255) NOT NULL,
"lastname" varchar(255) NOT NULL,
"email" varchar(255) NOT NULL,
"phoneNumber" varchar(255),
"description" text,
"companyName" varchar(255),
"companyOverview" text,
"companyWebsite" varchar(255),
"companyLocation" varchar(255),
"offeredServices" text,
"areasServed" jsonb,
"hasProfile" boolean,
"hasCompanyLogo" boolean,
"licensedIn" jsonb,
"gender" "gender",
"created" timestamp,
"updated" timestamp
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "businesses" ADD CONSTRAINT "businesses_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "commercials" ADD CONSTRAINT "commercials_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -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";

View File

@@ -0,0 +1,504 @@
{
"id": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "5",
"dialect": "pg",
"tables": {
"businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_userId_users_id_fk": {
"name": "businesses_userId_users_id_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"serial_id": {
"name": "serial_id",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"website": {
"name": "website",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_userId_users_id_fk": {
"name": "commercials_userId_users_id_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"gender": {
"name": "gender",
"values": {
"male": "male",
"female": "female"
}
}
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,518 @@
{
"id": "0bc02618-4414-4e90-8c44-808737611da7",
"prevId": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
"version": "5",
"dialect": "pg",
"tables": {
"businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_userId_users_id_fk": {
"name": "businesses_userId_users_id_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"serial_id": {
"name": "serial_id",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"website": {
"name": "website",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_userId_users_id_fk": {
"name": "commercials_userId_users_id_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"primaryKey": false,
"notNull": false
},
"customerType": {
"name": "customerType",
"type": "customerType",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"customerType": {
"name": "customerType",
"values": {
"buyer": "buyer",
"broker": "broker",
"professional": "professional"
}
},
"gender": {
"name": "gender",
"values": {
"male": "male",
"female": "female"
}
}
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,20 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1716495198537,
"tag": "0000_burly_bruce_banner",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1717085220861,
"tag": "0001_wet_mephistopheles",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,86 @@
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';
export const PG_CONNECTION = 'PG_CONNECTION';
export const genderEnum = pgEnum('gender', ['male', 'female']);
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'broker', 'professional']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
firstname: varchar('firstname', { length: 255 }).notNull(),
lastname: varchar('lastname', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull(),
phoneNumber: varchar('phoneNumber', { length: 255 }),
description: text('description'),
companyName: varchar('companyName', { length: 255 }),
companyOverview: text('companyOverview'),
companyWebsite: varchar('companyWebsite', { length: 255 }),
companyLocation: varchar('companyLocation', { length: 255 }),
offeredServices: text('offeredServices'),
areasServed: jsonb('areasServed').$type<AreasServed[]>(),
hasProfile: boolean('hasProfile'),
hasCompanyLogo: boolean('hasCompanyLogo'),
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
gender: genderEnum('gender'),
customerType: customerTypeEnum('customerType'),
created: timestamp('created'),
updated: timestamp('updated'),
});
export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('userId').references(() => users.id),
type: integer('type'),
title: varchar('title', { length: 255 }),
description: text('description'),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
price: doublePrecision('price'),
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
draft: boolean('draft'),
listingsCategory: varchar('listingsCategory', { length: 255 }),
realEstateIncluded: boolean('realEstateIncluded'),
leasedLocation: boolean('leasedLocation'),
franchiseResale: boolean('franchiseResale'),
salesRevenue: doublePrecision('salesRevenue'),
cashFlow: doublePrecision('cashFlow'),
supportAndTraining: text('supportAndTraining'),
employees: integer('employees'),
established: integer('established'),
internalListingNumber: integer('internalListingNumber'),
reasonForSale: varchar('reasonForSale', { length: 255 }),
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
internals: text('internals'),
imageName: varchar('imagePath', { length: 200 }),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
});
export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(),
serialId: serial('serial_id'),
userId: uuid('userId').references(() => users.id),
type: integer('type'),
title: varchar('title', { length: 255 }),
description: text('description'),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
price: doublePrecision('price'),
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
listingsCategory: varchar('listingsCategory', { length: 255 }),
hideImage: boolean('hideImage'),
draft: boolean('draft'),
zipCode: integer('zipCode'),
county: varchar('county', { length: 255 }),
email: varchar('email', { length: 255 }),
website: varchar('website', { length: 255 }),
phoneNumber: varchar('phoneNumber', { length: 255 }),
imageOrder: varchar('imageOrder', { length: 200 }).array(),
imagePath: varchar('imagePath', { length: 200 }),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
});

View File

@@ -1,13 +1,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { fstat, readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { ImageProperty } from '../models/main.model.js';
import sharp from 'sharp';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import path, { join } from 'path';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ImageProperty, Subscription } from '../models/main.model.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -21,74 +20,86 @@ 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');
this.subscriptions = JSON.parse(rawData); this.subscriptions = JSON.parse(rawData);
} }
getSubscriptions() { 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`) return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
} }
// ############
async getPropertyImages(listingId: string): Promise<ImageProperty[]> { // Property
const result: ImageProperty[] = [] // ############
const directory = `./pictures/property/${listingId}` async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
const result: string[] = [];
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 => {
const image: ImageProperty = { name: f, id: '', code: '' }; result.push(f);
result.push(image) });
})
return result; return result;
} else { } else {
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;
} else { } else {
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);
@@ -104,7 +115,7 @@ export class FileService {
return null; return null;
} }
} }
async resizeImageToAVIF(buffer: Buffer, maxSize: number,imageName:string,directory:string) { async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) {
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
let output; let output;
let start = Date.now(); let start = Date.now();
@@ -115,26 +126,24 @@ export class FileService {
.toBuffer(); .toBuffer();
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
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){ deleteImage(path: 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){
fs.unlinkSync(path); fs.unlinkSync(path);
} }
deleteDirectoryIfExists(imagePath) {
const dirPath = `pictures/property/${imagePath}`;
try {
const exists = fs.pathExistsSync();
if (exists) {
fs.removeSync(dirPath);
console.log(`Directory ${dirPath} was deleted.`);
} else {
console.log(`Directory ${dirPath} does not exist.`);
}
} catch (error) {
console.error(`Error while deleting ${dirPath}:`, error);
}
}
} }

View File

@@ -14,7 +14,7 @@ export class GeoService {
this.loadGeo(); this.loadGeo();
} }
private loadGeo(): void { private loadGeo(): void {
const filePath = join(__dirname,'..', 'assets', 'geo.json'); const filePath = join(__dirname,'../..', 'assets', 'geo.json');
const rawData = readFileSync(filePath, 'utf8'); const rawData = readFileSync(filePath, 'utf8');
this.geo = JSON.parse(rawData); this.geo = JSON.parse(rawData);
} }

View File

@@ -1,74 +1,55 @@
import { Body, 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 { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
import { ListingsService } from '../listings/listings.service.js'; import { ListingsService } from '../listings/listings.service.js';
import { CommercialPropertyListing } from 'src/models/main.model.js'; import { SelectOptionsService } from '../select-options/select-options.service.js';
import { Entity, EntityData } from 'redis-om';
@Controller('image') @Controller('image')
export class ImageController { export class ImageController {
constructor(
constructor(private fileService:FileService, private fileService: FileService,
private listingService:ListingsService, private listingService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
private selectOptions:SelectOptionsService) { private selectOptions: SelectOptionsService,
) {}
// ############
// Property
// ############
@Post('uploadPropertyPicture/:imagePath/:serial')
@UseInterceptors(FileInterceptor('file'))
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
await this.listingService.addImage(imagePath, serial, imagename);
} }
@Delete('propertyPicture/:imagePath/:serial/:imagename')
@Post('uploadPropertyPicture/:id') async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
@UseInterceptors(FileInterceptor('file'),) this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { await this.listingService.deleteImage(imagePath, serial, imagename);
const imagename = await this.fileService.storePropertyPicture(file,id);
await this.listingService.addImage(id,imagename);
} }
// ############
@Post('uploadProfile/:id') // Profile
@UseInterceptors(FileInterceptor('file'),) // ############
async uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { @Post('uploadProfile/:email')
await this.fileService.storeProfilePicture(file,id); @UseInterceptors(FileInterceptor('file'))
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
await this.fileService.storeProfilePicture(file, adjustedEmail);
} }
@Delete('profile/:email/')
@Post('uploadCompanyLogo/:id') async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
@UseInterceptors(FileInterceptor('file'),) this.fileService.deleteImage(`pictures/profile/${email}.avif`);
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
await this.fileService.storeCompanyLogo(file,id);
} }
// ############
@Get(':id') // Logo
async getPropertyImagesById(@Param('id') id:string): Promise<any> { // ############
const result = await this.listingService.getCommercialPropertyListingById(id); @Post('uploadCompanyLogo/:email')
const listing = result as CommercialPropertyListing; @UseInterceptors(FileInterceptor('file'))
if (listing.imageOrder){ async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
return listing.imageOrder await this.fileService.storeCompanyLogo(file, adjustedEmail);
} else {
const imageOrder = await this.fileService.getPropertyImages(id);
listing.imageOrder=imageOrder;
this.listingService.saveListing(listing);
return imageOrder;
} }
} @Delete('logo/:email/')
@Get('profileImages/:userids') async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
async getProfileImagesForUsers(@Param('userids') userids:string): Promise<any> { this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
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/:listingid/:imagename')
async deletePropertyImagesById(@Param('listingid') listingid:string,@Param('imagename') imagename:string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`);
await this.listingService.deleteImage(listingid,imagename);
}
@Delete('logo/:userid/')
async deleteLogoImagesById(@Param('id') id:string): Promise<any> {
this.fileService.deleteImage(`pictures/property//${id}`)
}
@Delete('profile/:userid/')
async deleteProfileImagesById(@Param('id') id:string): Promise<any> {
this.fileService.deleteImage(`pictures/property//${id}`)
} }
} }

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -1,54 +1,52 @@
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 { FileService } from '../file/file.service.js';
import { convertStringToNullUndefined } from '../utils.js';
import { ListingsService } from './listings.service.js';
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 { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { JwtUser, ListingCriteria } from '../models/main.model.js';
import { ListingsService } from './listings.service.js';
@Controller('listings/business') @Controller('listings/business')
export class BusinessListingsController { export class BusinessListingsController {
constructor(
private readonly listingsService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { @UseGuards(OptionalJwtAuthGuard)
}
@Get()
findAll(): any {
return this.listingsService.getAllBusinessListings();
}
@Get(':id') @Get(':id')
findById(@Param('id') id:string): any { findById(@Request() req, @Param('id') id: string): any {
return this.listingsService.getBusinessListingById(id); return this.listingsService.findBusinessesById(id, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard)
@Get('user/:userid') @Get('user/:userid')
findByUserId(@Param('userid') userid:string): any { findByUserId(@Request() req, @Param('userid') userid: string): any {
return this.listingsService.getBusinessListingByUserId(userid); return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard)
@Post('search') @Post('search')
find(@Body() criteria: any): any { find(@Request() req, @Body() criteria: ListingCriteria): any {
return this.listingsService.findBusinessListings(criteria); return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
} }
/**
* @param listing creates a new listing
*/
@Post() @Post()
save(@Body() listing: any){ create(@Body() listing: any) {
this.logger.info(`Save Listing`); this.logger.info(`Save Listing`);
this.listingsService.saveListing(listing) return this.listingsService.createListing(listing, businesses);
}
@Put()
update(@Body() listing: any) {
this.logger.info(`Save Listing`);
return this.listingsService.updateBusinessListing(listing.id, listing);
} }
/**
* @param id deletes a listing
*/
@Delete(':id') @Delete(':id')
deleteById(@Param('id') id:string){ deleteById(@Param('id') id: string) {
this.listingsService.deleteBusinessListing(id) this.listingsService.deleteListing(id, businesses);
} }
@Get('states/all')
@Delete('deleteAll') getStates(): any {
deleteAll(){ return this.listingsService.getStates(businesses);
this.listingsService.deleteAllBusinessListings()
} }
} }

View File

@@ -1,51 +1,54 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { ListingsService } from './listings.service.js';
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 { FileInterceptor } from '@nestjs/platform-express'; import { commercials } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { CommercialPropertyListing, ImageProperty } from 'src/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';
@Controller('listings/commercialProperty') @Controller('listings/commercialProperty')
export class CommercialPropertyListingsController { export class CommercialPropertyListingsController {
constructor(
private readonly listingsService: ListingsService,
private fileService: FileService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
constructor(private readonly listingsService:ListingsService,private fileService:FileService,@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.getCommercialPropertyListingById(id); return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard)
@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')
find(@Body() criteria: any): any { async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
return this.listingsService.findCommercialPropertyListings(criteria); return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
} }
@Get('states/all')
@Put('imageOrder/:id') getStates(): any {
async changeImageOrder(@Param('id') id:string,@Body() imageOrder: ImageProperty[]) { return this.listingsService.getStates(commercials);
this.listingsService.updateImageOrder(id, imageOrder)
} }
/**
* @param listing creates a new listing
*/
@Post() @Post()
save(@Body() listing: any){ async create(@Body() listing: any) {
this.logger.info(`Save Listing`); this.logger.info(`Save Listing`);
this.listingsService.saveListing(listing) return await this.listingsService.createListing(listing, commercials);
} }
@Put()
/** async update(@Body() listing: any) {
* @param id deletes a listing this.logger.info(`Save Listing`);
*/ return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
@Delete(':id')
deleteById(@Param('id') id:string){
this.listingsService.deleteCommercialPropertyListing(id)
} }
@Delete('deleteAll') @Delete(':id/:imagePath')
deleteAll(){ deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
this.listingsService.deleteAllcommercialListings() this.listingsService.deleteListing(id, commercials);
this.fileService.deleteDirectoryIfExists(imagePath);
} }
} }

View File

@@ -1,18 +1,18 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BusinessListingsController } from './business-listings.controller.js'; import { AuthModule } from '../auth/auth.module.js';
import { ListingsService } from './listings.service.js'; import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
import { RedisModule } from '../redis/redis.module.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { UnknownListingsController } from './unknown-listings.controller.js';
import { UserModule } from '../user/user.module.js';
import { BrokerListingsController } from './broker-listings.controller.js';
import { UserService } from '../user/user.service.js'; import { UserService } from '../user/user.service.js';
import { BrokerListingsController } from './broker-listings.controller.js';
import { BusinessListingsController } from './business-listings.controller.js';
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
import { ListingsService } from './listings.service.js';
import { UnknownListingsController } from './unknown-listings.controller.js';
@Module({ @Module({
imports: [RedisModule], 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],
}) })
export class ListingsModule {} export class ListingsModule {}

View File

@@ -1,186 +1,185 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm';
BusinessListing, import { NodePgDatabase } from 'drizzle-orm/node-postgres';
CommercialPropertyListing,
ListingCriteria,
ListingType,
ImageProperty,
ListingCategory
} from '../models/main.model.js';
import { convertStringToNullUndefined } from '../utils.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { EntityData, EntityId, Repository, Schema, SchemaDefinition } from 'redis-om'; import * as schema from '../drizzle/schema.js';
import { REDIS_CLIENT } from '../redis/redis.module.js'; import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.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 {
schemaNameBusiness:ListingCategory={name:'business'} constructor(
schemaNameCommercial:ListingCategory={name:'commercialProperty'} @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
businessListingRepository:Repository; @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
commercialPropertyListingRepository:Repository; private fileService: FileService,
baseListingSchemaDef : SchemaDefinition = { ) {}
id: { type: 'string' }, private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
userId: { type: 'string' }, const conditions = [];
listingsCategory: { type: 'string' }, if (criteria.type) {
title: { type: 'string' }, conditions.push(eq(table.type, criteria.type));
description: { type: 'string' },
country: { type: 'string' },
state:{ type: 'string' },
city:{ type: 'string' },
zipCode: { type: 'number' },
type: { type: 'string' },
price: { type: 'number' },
favoritesForUser:{ type: 'string[]' },
hideImage:{ type: 'boolean' },
draft:{ type: 'boolean' },
created:{ type: 'date' },
updated:{ type: 'date' }
} }
businessListingSchemaDef : SchemaDefinition = { if (criteria.state) {
...this.baseListingSchemaDef, conditions.push(eq(table.state, criteria.state));
salesRevenue: { type: 'number' },
cashFlow: { type: 'number' },
employees: { type: 'number' },
established: { type: 'number' },
internalListingNumber: { type: 'number' },
realEstateIncluded:{ type: 'boolean' },
leasedLocation:{ type: 'boolean' },
franchiseResale:{ type: 'boolean' },
supportAndTraining: { type: 'string' },
reasonForSale: { type: 'string' },
brokerLicencing: { type: 'string' },
internals: { type: 'string' },
} }
commercialPropertyListingSchemaDef : SchemaDefinition = { if (criteria.minPrice) {
...this.baseListingSchemaDef, conditions.push(gte(table.price, criteria.minPrice));
imageNames:{ type: 'string[]' },
} }
businessListingSchema = new Schema(this.schemaNameBusiness.name,this.businessListingSchemaDef, { if (criteria.maxPrice) {
dataStructure: 'JSON' conditions.push(lte(table.price, criteria.maxPrice));
})
commercialPropertyListingSchema = new Schema(this.schemaNameCommercial.name,this.commercialPropertyListingSchemaDef, {
dataStructure: 'JSON'
})
constructor(@Inject(REDIS_CLIENT) private readonly redis: any, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger){
this.businessListingRepository = new Repository(this.businessListingSchema, redis);
this.commercialPropertyListingRepository = new Repository(this.commercialPropertyListingSchema, redis)
this.businessListingRepository.createIndex();
this.commercialPropertyListingRepository.createIndex();
} }
async saveListing(listing: BusinessListing | CommercialPropertyListing) { if (criteria.realEstateChecked) {
const repo=listing.listingsCategory==='business'?this.businessListingRepository:this.commercialPropertyListingRepository; conditions.push(eq(businesses.realEstateIncluded, true));
let result
if (listing.id){
result = await repo.save(listing.id,listing as any)
} else {
result = await repo.save(listing as any)
listing.id=result[EntityId];
result = await repo.save(listing.id,listing as any)
} }
return result; if (criteria.title) {
conditions.push(ilike(table.title, `%${criteria.title}%`));
} }
async getCommercialPropertyListingById(id: string): Promise<CommercialPropertyListing>{ return conditions;
return await this.commercialPropertyListingRepository.fetch(id) as unknown as CommercialPropertyListing;
} }
async getBusinessListingById(id: string) { // ##############################################################
return await this.businessListingRepository.fetch(id) // Listings general
// ##############################################################
async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria, commercials, user);
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(or(eq(commercials.draft, false), eq(commercials.imagePath, emailToDirName(user?.username))));
} }
async getBusinessListingByUserId(userid:string){ const [data, total] = await Promise.all([
return await this.businessListingRepository.search().where('userId').equals(userid).return.all() this.conn
.select()
.from(commercials)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(commercials)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
} }
async deleteBusinessListing(id: string){ async findBusinessListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
return await this.businessListingRepository.remove(id); const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria, businesses, user);
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(or(eq(businesses.draft, false), eq(businesses.imageName, emailToDirName(user?.username))));
} }
async deleteCommercialPropertyListing(id: string){ const [data, total] = await Promise.all([
return await this.commercialPropertyListingRepository.remove(id); 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 getAllBusinessListings(start?: number, end?: number) { async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
return await this.businessListingRepository.search().return.all() 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 getAllCommercialListings(start?: number, end?: number) { async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
return await this.commercialPropertyListingRepository.search().return.all() 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 findBusinessListings(criteria:ListingCriteria): Promise<any> { async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
// let listings = await this.getAllBusinessListings(); const conditions = [];
// return this.find(criteria,listings); conditions.push(eq(commercials.imagePath, emailToDirName(email)));
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`); if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
const result = await this.redis.ft.search('business:index','*',{LIMIT:{from:0,size:50}}); conditions.push(ne(commercials.draft, true));
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`);
return result.documents;
} }
async findCommercialPropertyListings(criteria:ListingCriteria): Promise<any> { return (await this.conn
let listings = await this.getAllCommercialListings(); .select()
return this.find(criteria,listings); .from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
} }
async deleteAllBusinessListings(){ async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const ids = await this.getIdsForRepo(this.schemaNameBusiness.name); const conditions = [];
this.businessListingRepository.remove(ids); conditions.push(eq(businesses.imageName, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(businesses.draft, true));
} }
async deleteAllcommercialListings(){ return (await this.conn
const ids = await this.getIdsForRepo(this.schemaNameCommercial.name); .select()
this.commercialPropertyListingRepository.remove(ids); .from(businesses)
.where(and(...conditions))) as CommercialPropertyListing[];
} }
async getIdsForRepo(repoName:string, maxcount=100000){ async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
let cursor = 0; const result = await this.conn
let ids = []; .select()
do { .from(commercials)
const reply = await this.redis.scan(cursor, { .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
MATCH: `${repoName}:*`, return result[0] as CommercialPropertyListing;
COUNT: maxcount }
}); async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
cursor = reply.cursor; data.created = new Date();
// Extrahiere die ID aus jedem Schlüssel und füge sie zur Liste hinzu data.updated = new Date();
ids = ids.concat(reply.keys.map(key => key.split(':')[1]).filter(id=>id!='index')); const [createdListing] = await this.conn.insert(table).values(data).returning();
} while (cursor !== 0); return createdListing as BusinessListing | CommercialPropertyListing;
return ids;
} }
async find(criteria:ListingCriteria, listings: any[]): Promise<any> { async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory); data.updated = new Date();
if (convertStringToNullUndefined(criteria.type)){ data.created = new Date(data.created);
console.log(criteria.type); const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
listings=listings.filter(l=>l.type===criteria.type); 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;
} }
if (convertStringToNullUndefined(criteria.state)){ const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
console.log(criteria.state); return updateListing as BusinessListing | CommercialPropertyListing;
listings=listings.filter(l=>l.state===criteria.state);
} }
if (convertStringToNullUndefined(criteria.minPrice)){ async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
console.log(criteria.minPrice); data.updated = new Date();
listings=listings.filter(l=>l.price>=Number(criteria.minPrice)); 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;
} }
if (convertStringToNullUndefined(criteria.maxPrice)){ async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
console.log(criteria.maxPrice); await this.conn.delete(table).where(eq(table.id, id));
listings=listings.filter(l=>l.price<=Number(criteria.maxPrice));
} }
if (convertStringToNullUndefined(criteria.realEstateChecked)){ async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
console.log(criteria.realEstateChecked); return await this.conn
listings=listings.filter(l=>l.realEstateIncluded); .select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
.from(table)
.groupBy(sql`${table.state}`)
.orderBy(sql`count desc`);
} }
if (convertStringToNullUndefined(criteria.category)){ // ##############################################################
console.log(criteria.category); // Images for commercial Properties
listings=listings.filter(l=>l.category===criteria.category); // ##############################################################
} async deleteImage(imagePath: string, serial: string, name: string) {
return listings const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
} const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) {
async updateImageOrder(id:string,imageOrder: ImageProperty[]){ listing.imageOrder.splice(index, 1);
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing await this.updateCommercialPropertyListing(listing.id, listing);
listing.imageOrder=imageOrder;
this.saveListing(listing);
}
async deleteImage(listingid:string,name:string,){
const listing = await this.getCommercialPropertyListingById(listingid) as unknown as CommercialPropertyListing
const index = listing.imageOrder.findIndex(im=>im.name===name);
if (index>-1){
listing.imageOrder.splice(index,1);
this.saveListing(listing);
} }
} }
async addImage(id:string,imagename: string){ async addImage(imagePath: string, serial: string, imagename: string) {
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
listing.imageOrder.push({name:imagename,code:'',id:''}); listing.imageOrder.push(imagename);
this.saveListing(listing); await this.updateCommercialPropertyListing(listing.id, listing);
} }
} }

View File

@@ -1,29 +1,22 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common'; import { Controller, Inject } from '@nestjs/common';
import { FileService } from '../file/file.service.js';
import { convertStringToNullUndefined } from '../utils.js';
import { ListingsService } from './listings.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ListingsService } from './listings.service.js';
@Controller('listings/undefined') @Controller('listings/undefined')
export class UnknownListingsController { export class UnknownListingsController {
constructor(
private readonly listingsService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { // @Get(':id')
} // async findById(@Param('id') id: string): Promise<any> {
// const result = await this.listingsService.findById(id, businesses);
// if (result) {
@Get(':id') // return result;
async findById(@Param('id') id:string): Promise<any> { // } else {
const result = await this.listingsService.getBusinessListingById(id); // return await this.listingsService.findById(id, commercials);
if (result.id){ // }
return result // }
} else {
return await this.listingsService.getCommercialPropertyListingById(id);
}
}
@Get('repo/:repo')
async getAllByRepo(@Param('repo') repo:string): Promise<any> {
return await this.listingsService.getIdsForRepo(repo);
}
} }

View File

@@ -1,15 +1,16 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { Body, Controller, Post } from '@nestjs/common';
import { ErrorResponse, MailInfo } from 'src/models/main.model.js';
import { MailService } from './mail.service.js'; import { MailService } from './mail.service.js';
import { KeycloakUser, MailInfo } from 'src/models/main.model.js';
@Controller('mail') @Controller('mail')
export class MailController { export class MailController {
constructor(private mailService:MailService){ constructor(private mailService: MailService) {}
}
@Post() @Post()
sendEMail(@Body() mailInfo: MailInfo): Promise< KeycloakUser> { sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
if (mailInfo.listing) {
return this.mailService.sendInquiry(mailInfo); return this.mailService.sendInquiry(mailInfo);
} else {
return this.mailService.sendRequest(mailInfo);
}
} }
} }

View File

@@ -1,26 +1,27 @@
import { Module } from '@nestjs/common';
import { MailService } from './mail.service.js';
import { MailController } from './mail.controller.js';
import { MailerModule } from '@nestjs-modules/mailer'; import { MailerModule } from '@nestjs-modules/mailer';
import path, { join } from 'path';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
import { Module } from '@nestjs/common';
import path, { join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { AuthModule } from '../auth/auth.module.js'; import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.service.js';
import { UserModule } from '../user/user.module.js';
import { UserService } from '../user/user.service.js';
import { MailController } from './mail.controller.js';
import { MailService } from './mail.service.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const user = process.env.amazon_user;
const password = process.env.amazon_password;
@Module({ @Module({
imports: [AuthModule, imports: [
DrizzleModule,
UserModule,
MailerModule.forRoot({ MailerModule.forRoot({
// transport: 'smtps://user@example.com:topsecret@smtp.example.com',
// or
transport: { transport: {
host: 'email-smtp.us-east-2.amazonaws.com', host: 'email-smtp.us-east-2.amazonaws.com',
secure: false, secure: false,
port:587, port: 587,
// auth: {
// user: 'andreas.knuth@gmail.com',
// pass: 'ksnh xjae dqbv xana',
// },
auth: { auth: {
user: 'AKIAU6GDWVAQ2QNFLNWN', user: 'AKIAU6GDWVAQ2QNFLNWN',
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7', pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
@@ -38,7 +39,7 @@ const __dirname = path.dirname(__filename);
}, },
}), }),
], ],
providers: [MailService], providers: [MailService, UserService, FileService],
controllers: [MailController] controllers: [MailController],
}) })
export class MailModule {} export class MailModule {}

View File

@@ -1,26 +1,63 @@
import { MailerService } from '@nestjs-modules/mailer'; import { MailerService } from '@nestjs-modules/mailer';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth/auth.service.js'; import path, { join } from 'path';
import { KeycloakUser, MailInfo, User } from '../models/main.model.js'; import { fileURLToPath } from 'url';
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js';
import { UserService } from '../user/user.service.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@Injectable() @Injectable()
export class MailService { export class MailService {
constructor(private mailerService: MailerService, private authService:AuthService) {} constructor(
private mailerService: MailerService,
private userService: UserService,
) {}
async sendInquiry(mailInfo: MailInfo):Promise<KeycloakUser> { async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser; //const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser;
const user = await this.userService.getUserByMail(mailInfo.email);
console.log(JSON.stringify(user)); console.log(JSON.stringify(user));
if (isEmpty(mailInfo.sender.name)) {
return { fields: [{ fieldname: 'name', message: 'Required' }] };
}
await this.mailerService.sendMail({ await this.mailerService.sendMail({
to: user.email, to: user.email,
from: '"Bizmatch Team" <info@bizmatch.net>', // override default from from: '"Bizmatch Team" <info@bizmatch.net>', // override default from
subject: `Inquiry from ${mailInfo.sender.name}`, subject: `Inquiry from ${mailInfo.sender.name}`,
template: './inquiry', // `.hbs` extension is appended automatically //template: './inquiry', // `.hbs` extension is appended automatically
context: { // ✏️ filling curly brackets with content template: join(__dirname, '../..', 'mail/templates/inquiry.hbs'),
name: user.firstName, context: {
inquiry:mailInfo.sender.comments // ✏️ filling curly brackets with content
name: user.firstname,
inquiry: mailInfo.sender.comments,
internalListingNumber: mailInfo.listing.internalListingNumber,
title: mailInfo.listing.title,
iname: mailInfo.sender.name,
phone: mailInfo.sender.phoneNumber,
email: mailInfo.sender.email,
id: mailInfo.listing.id,
url: mailInfo.url,
}, },
}); });
return user
} }
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,
},
});
}
}

View File

@@ -1,5 +1,104 @@
<p>Hey {{ name }},</p> <!DOCTYPE html>
<p>You got an inquiry a</p> <html lang="en">
<p> <head>
{{inquiry}} <meta charset="UTF-8">
</p> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notification: New Buyer Lead</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 buyer lead from the Bizmatch Network</div>
<div class="subheader">Dear {{name}},</div>
<p>You've received a message regarding your "{{title}}" listing.</p>
<div class="section">
<div class="section-title">Buyer 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">Comments:</span>
<span class="info-value">{{inquiry}}</span>
</div>
</div>
<div class="section">
<div class="section-title">Listing Information</div>
<div class="info">
<span class="info-label">Headline:</span>
<span class="info-value">{{title}}</span>
</div>
<div class="info">
<span class="info-label">Listing ID:</span>
<span class="info-value"><a href="{{url}}/listing/{{id}}">{{id}}</a></span>
</div>
{{#if internalListingNumber}}
<div class="info">
<span class="info-label">Ref ID:</span>
<span class="info-value">{{internalListingNumber}}</span>
</div>
{{/if}}
</div>
</div>
</body>
</html>

View 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>

View File

@@ -1,13 +1,19 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import { AppModule } from './app.module.js'; import { AppModule } from './app.module.js';
const __filename = fileURLToPath(import.meta.url);
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);

View File

@@ -0,0 +1,107 @@
export interface User {
id?: string;
firstname: string;
lastname: string;
email: string;
phoneNumber?: string;
description?: string;
companyName?: string;
companyOverview?: string;
companyWebsite?: string;
companyLocation?: string;
offeredServices?: string;
areasServed?: AreasServed[];
hasProfile?: boolean;
hasCompanyLogo?: boolean;
licensedIn?: LicensedIn[];
gender?: 'male' | 'female';
customerType?: 'buyer' | 'broker' | 'professional';
created?: Date;
updated?: Date;
}
export interface UserData {
id?: string;
firstname: string;
lastname: string;
email: string;
phoneNumber?: string;
description?: string;
companyName?: string;
companyOverview?: string;
companyWebsite?: string;
companyLocation?: string;
offeredServices?: string;
areasServed?: string[];
hasProfile?: boolean;
hasCompanyLogo?: boolean;
licensedIn?: string[];
gender?: 'male' | 'female';
customerType?: 'buyer' | 'broker' | 'professional';
created?: Date;
updated?: Date;
}
export interface BusinessListing {
id: string;
userId?: string;
type?: number;
title?: string;
description?: string;
city?: string;
state?: string;
price?: number;
favoritesForUser?: string[];
draft?: boolean;
realEstateIncluded?: boolean;
leasedLocation?: boolean;
franchiseResale?: boolean;
salesRevenue?: number;
cashFlow?: number;
supportAndTraining?: string;
employees?: number;
established?: number;
internalListingNumber?: number;
reasonForSale?: string;
brokerLicencing?: string;
internals?: string;
imageName?: string;
created?: Date;
updated?: Date;
visits?: number;
lastVisit?: Date;
listingsCategory?: 'commercialProperty' | 'business';
}
export interface CommercialPropertyListing {
id: string;
serialId?: number;
userId?: string;
type?: number;
title?: string;
description?: string;
city?: string;
state?: string;
price?: number;
favoritesForUser?: string[];
hideImage?: boolean;
draft?: boolean;
zipCode?: number;
county?: string;
email?: string;
website?: string;
phoneNumber?: string;
imageOrder?: string[];
imagePath?: string;
created?: Date;
updated?: Date;
visits?: number;
lastVisit?: Date;
listingsCategory?: 'commercialProperty' | 'business';
}
export interface AreasServed {
county: string;
state: string;
}
export interface LicensedIn {
registerNo: string;
state: string;
}

View File

@@ -1 +0,0 @@
../../../common-models/src/main.model.ts

View File

@@ -0,0 +1,222 @@
import { BusinessListing, CommercialPropertyListing, User } from './db.model';
export interface StatesResult {
state: string;
count: number;
}
export interface KeyValue {
name: string;
value: string;
}
export interface KeyValueRatio {
label: string;
value: number;
}
export interface KeyValueStyle {
name: string;
value: string;
icon: string;
bgColorClass: string;
textColorClass: string;
}
export type SelectOption<T = number> = {
value: T;
label: string;
};
export type ImageType = {
name: 'propertyPicture' | 'companyLogo' | 'profile';
upload: string;
delete: string;
};
export type ListingCategory = {
name: 'business' | 'commercialProperty';
};
export type ListingType = BusinessListing | CommercialPropertyListing;
export type ResponseBusinessListingArray = {
data: BusinessListing[];
total: number;
};
export type ResponseBusinessListing = {
data: BusinessListing;
};
export type ResponseCommercialPropertyListingArray = {
data: CommercialPropertyListing[];
total: number;
};
export type ResponseCommercialPropertyListing = {
data: CommercialPropertyListing;
};
export type ResponseUsersArray = {
data: User[];
total: number;
};
export interface ListingCriteria {
start: number;
length: number;
page: number;
pageCount: number;
type: number;
state: string;
minPrice: number;
maxPrice: number;
realEstateChecked: boolean;
title: string;
category: 'professional' | 'broker';
name: string;
}
export interface KeycloakUser {
id: string;
createdTimestamp?: number;
username?: string;
enabled?: boolean;
totp?: boolean;
emailVerified?: boolean;
firstName: string;
lastName: string;
email: string;
disableableCredentialTypes?: any[];
requiredActions?: any[];
notBefore?: number;
access?: Access;
}
export interface JwtUser {
userId: string;
username: string;
roles: string[];
}
export interface Access {
manageGroupMembership: boolean;
view: boolean;
mapRoles: boolean;
impersonate: boolean;
manage: boolean;
}
export interface Subscription {
id: string;
userId: string;
level: string;
start: Date;
modified: Date;
end: Date;
status: string;
invoices: Array<Invoice>;
}
export interface Invoice {
id: string;
date: Date;
price: number;
}
export interface JwtToken {
exp: number;
iat: number;
auth_time: number;
jti: string;
iss: string;
aud: string;
sub: string;
typ: string;
azp: string;
nonce: string;
session_state: string;
acr: string;
realm_access: Realmaccess;
resource_access: Resourceaccess;
scope: string;
sid: string;
email_verified: boolean;
name: string;
preferred_username: string;
given_name: string;
family_name: string;
email: 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 {
account: Realmaccess;
}
interface Realmaccess {
roles: string[];
}
export interface PageEvent {
first: number;
rows: number;
page: number;
pageCount: number;
}
export interface AutoCompleteCompleteEvent {
originalEvent: Event;
query: string;
}
export interface MailInfo {
sender: Sender;
userId: string;
email: string;
url: string;
listing?: BusinessListing;
}
export interface Sender {
name?: string;
email?: string;
phoneNumber?: string;
state?: string;
comments?: string;
}
export interface ImageProperty {
id: string;
code: string;
name: string;
}
export interface ErrorResponse {
fields?: FieldError[];
general?: string[];
}
export interface FieldError {
fieldname: string;
message: string;
}
export function isEmpty(value: any): boolean {
// Check for undefined or null
if (value === undefined || value === null) {
return true;
}
// Check for empty string or string with only whitespace
if (typeof value === 'string') {
return value.trim().length === 0;
}
// Check for number and NaN
if (typeof value === 'number') {
return isNaN(value);
}
// If it's not a string or number, it's not considered empty by this function
return false;
}
export function emailToDirName(email: string): string {
if (email === undefined || email === null) {
return null;
}
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');
// Entferne führende und nachfolgende Unterstriche
const trimmedEmail = sanitizedEmail.replace(/^_+|_+$/g, '');
// Ersetze mehrfache aufeinanderfolgende Unterstriche durch einen einzelnen Unterstrich
const normalizedEmail = trimmedEmail.replace(/_+/g, '_');
return normalizedEmail;
}

View File

@@ -1,7 +1,4 @@
import { Entity } from "redis-om"; import { Entity } from "redis-om";
import { UserBase } from "./main.model.js";
export interface Geo { export interface Geo {
id: number; id: number;
name: string; name: string;
@@ -65,6 +62,3 @@ export interface Timezone {
abbreviation: string; abbreviation: string;
tzName: string; tzName: string;
} }
export interface UserEntity extends UserBase, Entity {
licensedIn?: string[];
}

View File

@@ -1,29 +0,0 @@
// redis.module.ts
import { Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'REDIS_OPTIONS',
useValue: {
url: 'redis://localhost:6379'
}
},
{
inject: ['REDIS_OPTIONS'],
provide: 'REDIS_CLIENT',
useFactory: async (options: { url: string }) => {
const client = createClient(options);
await client.connect();
return client;
}
}],
exports:['REDIS_CLIENT']
})
export class RedisModule {}
export const REDIS_CLIENT = "REDIS_CLIENT";
// redis.service.ts
import { Injectable } from '@nestjs/common';
import { createClient } from 'redis';

View File

@@ -0,0 +1,25 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class RequestDurationMiddleware implements NestMiddleware {
private readonly logger = new Logger(RequestDurationMiddleware.name);
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
// const duration = Date.now() - start;
// this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
const duration = Date.now() - start;
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();
}
}

View File

@@ -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,
} };
} }
} }

View File

@@ -3,31 +3,30 @@ import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js';
@Injectable() @Injectable()
export class SelectOptionsService { export class SelectOptionsService {
constructor() {}
constructor() { }
public typesOfBusiness: Array<KeyValueStyle> = [ public typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon:'fa-solid fa-car',bgColorClass:'bg-green-100',textColorClass:'text-green-600' }, { name: 'Automotive', value: '1', icon: 'fa-solid fa-car', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' },
{ name: 'Industrial Services', value: '2', icon:'fa-solid fa-industry',bgColorClass:'bg-yellow-100',textColorClass:'text-yellow-600'}, { name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
{ name: 'Real Estate', value: '3' , icon:'pi pi-building',bgColorClass:'bg-blue-100',textColorClass:'text-blue-600'}, { name: 'Real Estate', value: '3', icon: 'pi pi-building', bgColorClass: 'bg-blue-100', textColorClass: 'text-blue-600' },
{ name: 'Uncategorized', value: '4' , icon:'pi pi-question',bgColorClass:'bg-cyan-100',textColorClass:'text-cyan-600'}, { name: 'Uncategorized', value: '4', icon: 'pi pi-question', bgColorClass: 'bg-cyan-100', textColorClass: 'text-cyan-600' },
{ name: 'Retail', value: '5' , icon:'fa-solid fa-money-bill-wave',bgColorClass:'bg-pink-100',textColorClass:'text-pink-600'}, { name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', bgColorClass: 'bg-pink-100', textColorClass: 'text-pink-600' },
{ name: 'Oilfield SVE and MFG.', value: '6' , icon:'fa-solid fa-oil-well',bgColorClass:'bg-indigo-100',textColorClass:'text-indigo-600'}, { name: 'Oilfield SVE and MFG.', value: '6', icon: 'fa-solid fa-oil-well', bgColorClass: 'bg-indigo-100', textColorClass: 'text-indigo-600' },
{ name: 'Service', value: '7' , icon:'fa-solid fa-umbrella',bgColorClass:'bg-teal-100',textColorClass:'text-teal-600'}, { name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', bgColorClass: 'bg-teal-100', textColorClass: 'text-teal-600' },
{ name: 'Advertising', value: '8' , icon:'fa-solid fa-rectangle-ad',bgColorClass:'bg-orange-100',textColorClass:'text-orange-600'}, { name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', bgColorClass: 'bg-orange-100', textColorClass: 'text-orange-600' },
{ name: 'Agriculture', value: '9' , icon:'fa-solid fa-wheat-awn',bgColorClass:'bg-bluegray-100',textColorClass:'text-bluegray-600'}, { name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', bgColorClass: 'bg-bluegray-100', textColorClass: 'text-bluegray-600' },
{ name: 'Franchise', value: '10' , icon:'pi pi-star',bgColorClass:'bg-purple-100',textColorClass:'text-purple-600'}, { name: 'Franchise', value: '10', icon: 'pi pi-star', bgColorClass: 'bg-purple-100', textColorClass: 'text-purple-600' },
{ name: 'Professional', value: '11' , icon:'fa-solid fa-user-gear',bgColorClass:'bg-gray-100',textColorClass:'text-gray-600'}, { name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', bgColorClass: 'bg-gray-100', textColorClass: 'text-gray-600' },
{ name: 'Manufacturing', value: '12' , icon:'fa-solid fa-industry',bgColorClass:'bg-red-100',textColorClass:'text-red-600'}, { name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', bgColorClass: 'bg-red-100', textColorClass: 'text-red-600' },
{ name: 'Food and Restaurant', value: '13' , icon:'fa-solid fa-utensils',bgColorClass:'bg-primary-100',textColorClass:'text-primary-600'}, { name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', bgColorClass: 'bg-primary-100', textColorClass: 'text-primary-600' },
]; ];
public typesOfCommercialProperty: Array<KeyValueStyle> = [ public typesOfCommercialProperty: Array<KeyValueStyle> = [
{ name: 'Retail', value: '100' , icon:'fa-solid fa-money-bill-wave',bgColorClass:'bg-pink-100',textColorClass:'text-pink-600'}, { name: 'Retail', value: '100', icon: 'fa-solid fa-money-bill-wave', bgColorClass: 'bg-pink-100', textColorClass: 'text-pink-600' },
{ name: 'Land', value: '101' , icon:'pi pi-building',bgColorClass:'bg-blue-100',textColorClass:'text-blue-600'}, { name: 'Land', value: '101', icon: 'pi pi-building', bgColorClass: 'bg-blue-100', textColorClass: 'text-blue-600' },
{ name: 'Industrial', value: '102', icon:'fa-solid fa-industry',bgColorClass:'bg-yellow-100',textColorClass:'text-yellow-600'}, { name: 'Industrial', value: '102', icon: 'fa-solid fa-industry', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
{ name: 'Office', value: '103' , icon:'fa-solid fa-umbrella',bgColorClass:'bg-teal-100',textColorClass:'text-teal-600'}, { name: 'Office', value: '103', icon: 'fa-solid fa-umbrella', bgColorClass: 'bg-teal-100', textColorClass: 'text-teal-600' },
{ name: 'Mixed Use', value: '104' , icon:'fa-solid fa-rectangle-ad',bgColorClass:'bg-orange-100',textColorClass:'text-orange-600'}, { name: 'Mixed Use', value: '104', icon: 'fa-solid fa-rectangle-ad', bgColorClass: 'bg-orange-100', textColorClass: 'text-orange-600' },
{ name: 'Multifamily', value: '105' , icon:'pi pi-star',bgColorClass:'bg-purple-100',textColorClass:'text-purple-600'}, { name: 'Multifamily', value: '105', icon: 'pi pi-star', bgColorClass: 'bg-purple-100', textColorClass: 'text-purple-600' },
{ name: 'Uncategorized', value: '106' , icon:'pi pi-question',bgColorClass:'bg-cyan-100',textColorClass:'text-cyan-600'}, { name: 'Uncategorized', value: '106', icon: 'pi pi-question', bgColorClass: 'bg-cyan-100', textColorClass: 'text-cyan-600' },
]; ];
public prices: Array<KeyValue> = [ public prices: Array<KeyValue> = [
{ name: '$100K', value: '100000' }, { name: '$100K', value: '100000' },
@@ -39,77 +38,75 @@ export class SelectOptionsService {
public listingCategories: Array<KeyValue> = [ public listingCategories: Array<KeyValue> = [
{ 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 imageTypes:ImageType[] = [ ];
{name:'propertyPicture',upload:'uploadPropertyPicture',delete:'propertyPicture'}, public gender: Array<KeyValue> = [
{name:'companyLogo',upload:'uploadCompanyLogo',delete:'logo'}, { name: 'Male', value: 'male' },
{name:'profile',upload:'uploadProfile',delete:'profile'}, { name: 'Female', value: 'female' },
] ];
public imageTypes: ImageType[] = [
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
{ name: 'companyLogo', upload: 'uploadCompanyLogo', delete: 'logo' },
{ name: 'profile', upload: 'uploadProfile', delete: 'profile' },
];
private usStates = [ private usStates = [
{ name: 'ALABAMA', abbreviation: 'AL'}, { name: 'ALABAMA', abbreviation: 'AL' },
{ name: 'ALASKA', abbreviation: 'AK'}, { name: 'ALASKA', abbreviation: 'AK' },
{ name: 'AMERICAN SAMOA', abbreviation: 'AS'}, { name: 'ARIZONA', abbreviation: 'AZ' },
{ name: 'ARIZONA', abbreviation: 'AZ'}, { name: 'ARKANSAS', abbreviation: 'AR' },
{ name: 'ARKANSAS', abbreviation: 'AR'}, { name: 'CALIFORNIA', abbreviation: 'CA' },
{ name: 'CALIFORNIA', abbreviation: 'CA'}, { name: 'COLORADO', abbreviation: 'CO' },
{ name: 'COLORADO', abbreviation: 'CO'}, { name: 'CONNECTICUT', abbreviation: 'CT' },
{ name: 'CONNECTICUT', abbreviation: 'CT'}, { name: 'DELAWARE', abbreviation: 'DE' },
{ name: 'DELAWARE', abbreviation: 'DE'}, { name: 'DISTRICT OF COLUMBIA', abbreviation: 'DC' },
{ name: 'DISTRICT OF COLUMBIA', abbreviation: 'DC'}, { name: 'FLORIDA', abbreviation: 'FL' },
{ name: 'FEDERATED STATES OF MICRONESIA', abbreviation: 'FM'}, { name: 'GEORGIA', abbreviation: 'GA' },
{ name: 'FLORIDA', abbreviation: 'FL'}, { name: 'GUAM', abbreviation: 'GU' },
{ name: 'GEORGIA', abbreviation: 'GA'}, { name: 'HAWAII', abbreviation: 'HI' },
{ name: 'GUAM', abbreviation: 'GU'}, { name: 'IDAHO', abbreviation: 'ID' },
{ name: 'HAWAII', abbreviation: 'HI'}, { name: 'ILLINOIS', abbreviation: 'IL' },
{ name: 'IDAHO', abbreviation: 'ID'}, { name: 'INDIANA', abbreviation: 'IN' },
{ name: 'ILLINOIS', abbreviation: 'IL'}, { name: 'IOWA', abbreviation: 'IA' },
{ name: 'INDIANA', abbreviation: 'IN'}, { name: 'KANSAS', abbreviation: 'KS' },
{ name: 'IOWA', abbreviation: 'IA'}, { name: 'KENTUCKY', abbreviation: 'KY' },
{ name: 'KANSAS', abbreviation: 'KS'}, { name: 'LOUISIANA', abbreviation: 'LA' },
{ name: 'KENTUCKY', abbreviation: 'KY'}, { name: 'MAINE', abbreviation: 'ME' },
{ name: 'LOUISIANA', abbreviation: 'LA'}, { name: 'MARYLAND', abbreviation: 'MD' },
{ name: 'MAINE', abbreviation: 'ME'}, { name: 'MASSACHUSETTS', abbreviation: 'MA' },
{ name: 'MARSHALL ISLANDS', abbreviation: 'MH'}, { name: 'MICHIGAN', abbreviation: 'MI' },
{ name: 'MARYLAND', abbreviation: 'MD'}, { name: 'MINNESOTA', abbreviation: 'MN' },
{ name: 'MASSACHUSETTS', abbreviation: 'MA'}, { name: 'MISSISSIPPI', abbreviation: 'MS' },
{ name: 'MICHIGAN', abbreviation: 'MI'}, { name: 'MISSOURI', abbreviation: 'MO' },
{ name: 'MINNESOTA', abbreviation: 'MN'}, { name: 'MONTANA', abbreviation: 'MT' },
{ name: 'MISSISSIPPI', abbreviation: 'MS'}, { name: 'NEBRASKA', abbreviation: 'NE' },
{ name: 'MISSOURI', abbreviation: 'MO'}, { name: 'NEVADA', abbreviation: 'NV' },
{ name: 'MONTANA', abbreviation: 'MT'}, { name: 'NEW HAMPSHIRE', abbreviation: 'NH' },
{ name: 'NEBRASKA', abbreviation: 'NE'}, { name: 'NEW JERSEY', abbreviation: 'NJ' },
{ name: 'NEVADA', abbreviation: 'NV'}, { name: 'NEW MEXICO', abbreviation: 'NM' },
{ name: 'NEW HAMPSHIRE', abbreviation: 'NH'}, { name: 'NEW YORK', abbreviation: 'NY' },
{ name: 'NEW JERSEY', abbreviation: 'NJ'}, { name: 'NORTH CAROLINA', abbreviation: 'NC' },
{ name: 'NEW MEXICO', abbreviation: 'NM'}, { name: 'NORTH DAKOTA', abbreviation: 'ND' },
{ name: 'NEW YORK', abbreviation: 'NY'}, { name: 'OHIO', abbreviation: 'OH' },
{ name: 'NORTH CAROLINA', abbreviation: 'NC'}, { name: 'OKLAHOMA', abbreviation: 'OK' },
{ name: 'NORTH DAKOTA', abbreviation: 'ND'}, { name: 'OREGON', abbreviation: 'OR' },
{ name: 'NORTHERN MARIANA ISLANDS', abbreviation: 'MP'}, { name: 'PALAU', abbreviation: 'PW' },
{ name: 'OHIO', abbreviation: 'OH'}, { name: 'PENNSYLVANIA', abbreviation: 'PA' },
{ name: 'OKLAHOMA', abbreviation: 'OK'}, { name: 'RHODE ISLAND', abbreviation: 'RI' },
{ name: 'OREGON', abbreviation: 'OR'}, { name: 'SOUTH CAROLINA', abbreviation: 'SC' },
{ name: 'PALAU', abbreviation: 'PW'}, { name: 'SOUTH DAKOTA', abbreviation: 'SD' },
{ name: 'PENNSYLVANIA', abbreviation: 'PA'}, { name: 'TENNESSEE', abbreviation: 'TN' },
{ name: 'PUERTO RICO', abbreviation: 'PR'}, { name: 'TEXAS', abbreviation: 'TX' },
{ name: 'RHODE ISLAND', abbreviation: 'RI'}, { name: 'UTAH', abbreviation: 'UT' },
{ name: 'SOUTH CAROLINA', abbreviation: 'SC'}, { name: 'VERMONT', abbreviation: 'VT' },
{ name: 'SOUTH DAKOTA', abbreviation: 'SD'}, { name: 'VIRGINIA', abbreviation: 'VA' },
{ name: 'TENNESSEE', abbreviation: 'TN'}, { name: 'WASHINGTON', abbreviation: 'WA' },
{ name: 'TEXAS', abbreviation: 'TX'}, { name: 'WEST VIRGINIA', abbreviation: 'WV' },
{ name: 'UTAH', abbreviation: 'UT'}, { name: 'WISCONSIN', abbreviation: 'WI' },
{ name: 'VERMONT', abbreviation: 'VT'}, { name: 'WYOMING', abbreviation: 'WY' },
{ name: 'VIRGIN ISLANDS', abbreviation: 'VI'}, ];
{ name: 'VIRGINIA', abbreviation: 'VA'}, public locations: Array<any> = [...this.usStates.map(state => ({ name: state.name, value: state.abbreviation }))].concat({ name: 'CANADA', value: 'CA' });
{ name: 'WASHINGTON', abbreviation: 'WA'},
{ name: 'WEST VIRGINIA', abbreviation: 'WV'},
{ name: 'WISCONSIN', abbreviation: 'WI'},
{ name: 'WYOMING', abbreviation: 'WY' }
]
public locations:Array<any> = [...this.usStates.map(state=>({name:state.name, value:state.abbreviation}))].concat({name:'CANADA',value:"CA"});
} }

View File

@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { FileService } from '../file/file.service.js';
@Controller('subscriptions')
export class SubscriptionsController {
constructor(private readonly fileService: FileService){}
@Get()
findAll(): any {
return this.fileService.getSubscriptions();
}
}

View File

@@ -1,14 +1,26 @@
import { Body, Controller, Get, Inject, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Get, Inject, Param, Post, Query } from '@nestjs/common';
import { UserService } from './user.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { User } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { UserEntity } from 'src/models/server.model.js'; import { FileService } from '../file/file.service.js';
import { Subscription } from '../models/main.model.js';
import { UserService } from './user.service.js';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
constructor(
private userService: UserService,
private fileService: FileService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
constructor(private userService: UserService, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} @Get()
findByMail(@Query('mail') mail: string): any {
this.logger.info(`Searching for user with EMail: ${mail}`);
const user = this.userService.getUserByMail(mail);
this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user;
}
@Get(':id') @Get(':id')
findById(@Param('id') id: string): any { findById(@Param('id') id: string): any {
this.logger.info(`Searching for user with ID: ${id}`); this.logger.info(`Searching for user with ID: ${id}`);
@@ -16,9 +28,8 @@ export class UserController {
this.logger.info(`Found user: ${JSON.stringify(user)}`); this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user; return user;
} }
@Post() @Post()
save(@Body() user: any): Promise<UserEntity> { save(@Body() user: any): Promise<User> {
this.logger.info(`Saving user: ${JSON.stringify(user)}`); this.logger.info(`Saving user: ${JSON.stringify(user)}`);
const savedUser = this.userService.saveUser(user); const savedUser = this.userService.saveUser(user);
this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`); this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);
@@ -32,5 +43,23 @@ export class UserController {
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`); this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
return foundUsers; return foundUsers;
} }
@Get('states/all')
async getStates(): Promise<any[]> {
this.logger.info(`Getting all states for users`);
const result = await this.userService.getStates();
this.logger.info(`Found ${result.length} entries`);
return result;
}
@Get('subscriptions/:id')
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
const subscriptions = this.fileService.getSubscriptions();
const user = await this.userService.getUserById(id);
subscriptions.forEach(s => {
s.userId = user.id;
s.start = user.created;
s.modified = user.created;
});
return subscriptions;
}
} }

View File

@@ -1,13 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.service.js';
import { UserController } from './user.controller.js'; import { UserController } from './user.controller.js';
import { UserService } from './user.service.js'; import { UserService } from './user.service.js';
import { RedisModule } from '../redis/redis.module.js';
import { FileService } from '../file/file.service.js';
@Module({ @Module({
imports: [RedisModule], imports: [DrizzleModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService,FileService] providers: [UserService, FileService],
}) })
export class UserModule { export class UserModule {}
}

View File

@@ -1,45 +1,87 @@
import { Get, Inject, Injectable, Param } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { createClient } from 'redis'; import { and, eq, ilike, or, sql } from 'drizzle-orm';
import { Entity, Repository, Schema } from 'redis-om'; import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import { ListingCriteria, User } from '../models/main.model.js'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { REDIS_CLIENT } from '../redis/redis.module.js'; import { Logger } from 'winston';
import { UserEntity } from '../models/server.model.js'; import * as schema 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 { ListingCriteria, emailToDirName } from '../models/main.model.js';
@Injectable() @Injectable()
export class UserService { export class UserService {
userRepository:Repository; constructor(
userSchema = new Schema('user',{ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
id: { type: 'string' }, @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
firstname: { type: 'string' }, private fileService: FileService,
lastname: { type: 'string' }, ) {}
email: { type: 'string' }, private getConditions(criteria: ListingCriteria): any[] {
phoneNumber: { type: 'string' }, const conditions = [];
companyOverview:{ type: 'string' }, if (criteria.state) {
companyWebsite:{ type: 'string' }, //conditions.push(sql`EXISTS (SELECT 1 FROM unnest(users."areasServed") AS area WHERE area LIKE '%' || ${criteria.state} || '%')`);
companyLocation:{ type: 'string' }, conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: criteria.state }])}`);
offeredServices:{ type: 'string' },
areasServed:{ type: 'string[]' },
names:{ type: 'string[]', path:'$.licensedIn.name' },
values:{ type: 'string[]', path:'$.licensedIn.value' }
}, {
dataStructure: 'JSON'
})
constructor(@Inject(REDIS_CLIENT) private readonly redis: any,private fileService:FileService){
this.userRepository = new Repository(this.userSchema, redis)
this.userRepository.createIndex();
} }
async getUserById( id:string){ if (criteria.name) {
const user = await this.userRepository.fetch(id) as UserEntity; conditions.push(or(ilike(schema.users.firstname, `%${criteria.name}%`), ilike(schema.users.lastname, `%${criteria.name}%`)));
user.hasCompanyLogo=this.fileService.hasCompanyLogo(id); }
user.hasProfile=this.fileService.hasProfile(id); return conditions;
}
async getUserByMail(email: string) {
const users = (await this.conn
.select()
.from(schema.users)
.where(sql`email = ${email}`)) as User[];
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user; return user;
} }
async saveUser(user:any):Promise<UserEntity>{ async getUserById(id: string) {
return await this.userRepository.save(user.id,user) as UserEntity const users = (await this.conn
.select()
.from(schema.users)
.where(sql`id = ${id}`)) as User[];
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user;
} }
async findUser(criteria:ListingCriteria){ async saveUser(user: any): Promise<User> {
return await this.userRepository.search().return.all(); if (user.id) {
user.created = new Date(user.created);
user.updated = new Date();
const [updateUser] = await this.conn.update(schema.users).set(user).where(eq(schema.users.id, user.id)).returning();
return updateUser as User;
} else {
user.created = new Date();
user.updated = new Date();
const [newUser] = await this.conn.insert(schema.users).values(user).returning();
return newUser as User;
}
}
async findUser(criteria: ListingCriteria) {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria);
const [data, total] = await Promise.all([
this.conn
.select()
.from(schema.users)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(schema.users)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
}
async getStates(): Promise<any[]> {
const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`;
const result = await this.conn.execute(query);
return result.rows;
} }
} }

View File

@@ -1,4 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] "exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
} }

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2021", "target": "ES2021",
"module": "Node16", "module": "ESNext",
"moduleResolution": "Node16", "moduleResolution": "Node",
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
@@ -16,7 +16,8 @@
"strictNullChecks": false, "strictNullChecks": false,
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": false, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": false, "noFallthroughCasesInSwitch": false,
"esModuleInterop":true
} }
} }

18
bizmatch/.prettierrc.json Normal file
View File

@@ -0,0 +1,18 @@
{
"arrowParens": "avoid",
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 220,
"proseWrap": "always",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

28
bizmatch/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"editor.suggestSelection": "first",
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
"explorer.confirmDelete": false,
"typescript.updateImportsOnFileMove.enabled": "always",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"prettier.printWidth": 240,
"git.autofetch": false,
"git.autorefresh": true
}

32
bizmatch/certs/cert.pem Normal file
View File

@@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFgTCCA2mgAwIBAgIUa+QJdPmuRDNbuf/nzb2J+6ii5nwwDQYJKoZIhvcNAQEL
BQAwUDELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzEQMA4GA1UEBwwHRVJLUkFU
SDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTI0MDUxMDEy
NTQxNVoXDTI1MDUxMDEyNTQxNVowUDELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05S
VzEQMA4GA1UEBwwHRVJLUkFUSDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxiTSQDCC/i3n
X6bMKpl0baUgjbzYDc7ZrvIYfj/t25sdv0E/07ysbXNuldzCX6Rnva/1wVZS30zy
vQm8cVM074oP9qy7wKeIU15nEwRe03P5zipix1WXGWXHY+ShZ2MHy/iDQ1XzpO3p
xXs2vxuJZSUoz1M0c+/pTWBx2D790l5qNkt2sbk5NaHfPQDuw+y2cXDqmJqcCi9I
rYbaQGhXeb/IRu8pW4UwasVsq7DxGlDX8k8Dva5O0Ixf+muqQELuMdeTtR/PoFxw
2F+qOUlS2ujuyQkLOvVZOTalxRfWMuexaQzLlQO91MDehTrOFuMUBCKhYztgZKe2
k9z0fTJmtLyxMPTQuZCv1Gnrw6hcVxjiFQ8YP2ni+ekb86dIA3llH8r+4xEGygfB
QxHiBH9uO8Q9MFpfU2CPE7GxQoB17fu4KqaK0ucVnNM+rJcsNom9svixb5C4CkS/
S1/KQVDi8mrYwQIOP+Y4YLuNvSvUlitZXq8h0ogVqNMl2+R0CYX4lk/mkOEeCeGW
yG4ek2GQxZNLAnoMoLb+kHnVhPaV0SWW052wvXZzOrIMrlkSZK6yYim3JPsD8hc/
284lNEFL3DknICPsVFd64LjwPxA0J5AqyhQAvpXyFVHUUA5+h2EATrBh/Fp9cw84
AkEeVArMWOlx5cg7nAdgQaD5XUaBp7kCAwEAAaNTMFEwHQYDVR0OBBYEFMSO9FoT
nqjHpniyExGf53tV/TAhMB8GA1UdIwQYMBaAFMSO9FoTnqjHpniyExGf53tV/TAh
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBADbsIroXoCIe8adn
zh/WNZUUXLDW3JU2QyyYKhnZgqE0Wqh5QLgNwd5ZfH3Iaqhf0xFH9jUeEAWWA17d
hVy4rWsC200DRZ5BYOaffqdflpDE3yAk+8p3kAWjogaCX1wvBuLZ9BHWpuqQ72ec
zYM2ag2wWfBpicXl4BaSnsx1xErxKvxgixgGy6BhcErmfnYtJDnU2Cl+MSMXb758
7hAl9JiJAH8OuaAjvbkhSVTZQFjDgCGHHQ8mR8IksWdGGe/LN/yoWc4lk7lv7vmu
YBfP/SNZvxBzZlw1fXcdz1Wirljy7yz3+s59Knkc2jysysFC4LkZlUy0unmGoPy0
D1XdXyDMy05eoUaeRnM0rfzUxYfXMA3sQlsWw7he6fD8YylVedXxd3mcfK0jle0y
VkDyreZ9+mc/4vmjW0KpOfFGvhhAS9L1D8K3bKpky3HoHSqK1Nb8Ymh/WkhOpHwg
unUyIKdRHvGeWkUXQaLbRKI6w2BQwT7oKDOD60cJG26U3XcYarevz9qHsZX865tj
4xZrp+IUr8OkYBnRrmx2TZ70goRXI77nHVzHmY+xHhjvPJOZOcUAvEHU+5VY3ucN
0noEqiYzb77LcqVbbL3cywDLiyfdx9/x8TU1iYPA+IMwhYb/tLBFzFWmR7znw6On
D775XK/EVryozX/6GmtG+XGZs+57
-----END CERTIFICATE-----

52
bizmatch/certs/key.pem Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDGJNJAMIL+Ledf
pswqmXRtpSCNvNgNztmu8hh+P+3bmx2/QT/TvKxtc26V3MJfpGe9r/XBVlLfTPK9
CbxxUzTvig/2rLvAp4hTXmcTBF7Tc/nOKmLHVZcZZcdj5KFnYwfL+INDVfOk7enF
eza/G4llJSjPUzRz7+lNYHHYPv3SXmo2S3axuTk1od89AO7D7LZxcOqYmpwKL0it
htpAaFd5v8hG7ylbhTBqxWyrsPEaUNfyTwO9rk7QjF/6a6pAQu4x15O1H8+gXHDY
X6o5SVLa6O7JCQs69Vk5NqXFF9Yy57FpDMuVA73UwN6FOs4W4xQEIqFjO2Bkp7aT
3PR9Mma0vLEw9NC5kK/UaevDqFxXGOIVDxg/aeL56Rvzp0gDeWUfyv7jEQbKB8FD
EeIEf247xD0wWl9TYI8TsbFCgHXt+7gqporS5xWc0z6slyw2ib2y+LFvkLgKRL9L
X8pBUOLyatjBAg4/5jhgu429K9SWK1leryHSiBWo0yXb5HQJhfiWT+aQ4R4J4ZbI
bh6TYZDFk0sCegygtv6QedWE9pXRJZbTnbC9dnM6sgyuWRJkrrJiKbck+wPyFz/b
ziU0QUvcOScgI+xUV3rguPA/EDQnkCrKFAC+lfIVUdRQDn6HYQBOsGH8Wn1zDzgC
QR5UCsxY6XHlyDucB2BBoPldRoGnuQIDAQABAoICAAazixrZqSyAj/E3unL8Yqgs
rAevKd15r/oPPQ3UCq7hNaXYxphaKri+7TALWdWTQWD0eQrTaRUdTJ5hHGr2xfUO
BdExcV4oLF+pczH89VoQc5Pp8hJMzkHxI8e4nU7aVhKrcoEOAKIE2+Guc6EOBN0T
Xyh352++XvUbfG40XzBEujHg5oBHQ+yQ73RoOisNL/RxPbXwkLN1eu9Hfs0r2j2H
Y3Yms47hV8xcpfq+jsD1mAAddQJuyUKbZMma55SpztWHtXqsO0Dwr25Z+e9bD/7Q
XvcUo7kYQC7DruKWFkv9cw4a/S2qhTqTVVNLNFoozu3+39dz1CRDWdTxZaFwWXHX
KWc/YRmi5gxjadF3F87Ur+DYBLUJT/Iz5iUfUOQ/OON4ARDUM0UNqC1DemNxxI53
eiCw21GYwIA/59w7cWHOHJZImXKBMVJ3ko7hjI1eo4LWPvDfQHlG+5Fo9vUNPk9c
GiIreWMIycc5yUm9y1zxTa5zJdOBS+dQD0T230++o9OPytAiz2wT4ChXBfm02WUs
0UaU3uGSDmsvt4WI9SjlvSQi12EVekYc86lmlpgtMxc6zBpwJaFSrZa+N4ax9A5P
hOOeO6jy5XjEHTcIW+Yhrb0PCEr/U2xSn+zfhclHGTuwQLTdB30SB1pPECtA1j2+
rdcFx9+1asoIlQxO+k2pAoIBAQDsQQesFELq96/+7rTSgN1lm19AqKvm102WiOHT
CgJhltH19PyOnO23MKQKoYabTsSH2kOpwaA3CKPTk1ecU0P6mptN1xxEN7NgkRKC
Wr/pilXKTJg98A/o0zbxZkeYmyT3x6XwWVU1w4msR5lQ7bH4eR6MaQDghAKTRrAS
XMRFM4WQBfWr6DLTtz6Am5o+dA5qr8iIdjqGn8CgzPPqFQ/1ItWRkR2eiaGBUUhY
2Z8sL+3WW8h0u5qULqIrpY4EjLfl68FFbYgoxSLskQMpX4H/fCc8s9XPn527Ckiv
UuoRP7wsdcpt3H3SGDB8ZePH2JAbeu7nauAYeoSuzmqLwsF9AoIBAQDWtF96PpHK
FGQ8Epy79FDuJ95qaMFrfHRhx60erb2hHdwUYpBgGXHzN2icdP6Jg90ZyLdU97xH
QSNrTLxeUYcCZf0pucPYMFvNdMhTXiYLVpfwkho7Wl4YmuXEOG5ZW4pp0+sgFF/X
V8p2hu5QART6JwVsyBrO2T7EoGVDppbbhzF6tXnuLDV/JiP6/QcEYMxitQ06s6B2
8MXIqqNbidaCoALmeDgzderSKmiSGHWcAO6mef+xh0qZMfpOjwVLRTQyheiHJaub
DkBbtNu91kPJoyyn5+dCbuuK+tOii3FSANBAFH19esZJfcuZWo9x9dqZsT0HZ0lE
tlUDXhGBrVPtAoIBAHfD86a5Ur8YtyCOVB5Oc23Z2OzHVPWd+dgxJgG9Fj3wnhmI
iyukxCFUyCQXhExhHuIbtKdu39BmUd6k2AoIb/Kvw8EvJkYy0n1GrdJlPNqgZSM7
twXXF8mYoUa46dyj8ZamoCl6r+akbLtoRIGxLcJfbCwT4vzuDvwoHoQAgQLvvmqn
isYN3Q5U25uIxiWY4eIVoJwFC2BJxfX+UDw/VyqW8RttLE29SaFr2jgogjd9SJ2d
Q75hiFhMV6u2rosB5wvoer6+awL4BN9WF/s2Tol8n8t3AxHQwb4a1YQDjWMXI0aK
pAcTerkxyAqYAGPEFjHIHSo1lMrz+SVAwOR+42UCggEAUbVxRId9WidqggYfSdRP
3GKl3V8ihPJnJDMmai96pE9Fyyg7g6cLW6ExmaFYoSLiyQY+5wIk0AU1IoeghFCI
jdwcfX2pz6OPvF/+QOPqnJQG3NHtU7svZjPEz2kebblNsrqol5vJYZ2SeosdNKtE
vXKOOPjqYuAAaDoWb6l9bexEY0ufLIn8jfgI52LWAc+I2OPINhfYMIuu6ZAu/Q42
6Z1VnToRQVxV0ke7ZiYS1Bzytb5mFbzEIgsIFE+PlzauB7A4bv5iEW9aBMyOd++L
+rezre6ubvThhRGx6wEgTjHrDwf9Pfy0a5GJI0J4pskGuUjfTer70j+FmPN6vBwn
fQKCAQANeREfOILt9Unwpbo9Vj48BMfVYvJN7Gk4K6LGWN0rE0jxtpAzBiI3BqI0
+oj1gy+6Nn9n4hbeqDSyVB5uivCxmFIXpPO1s8Xu+EpEm90Po/551wWBvePOe8YK
vJK+UqUXDDcG+CUKsY8quOrNFIbSu4vOB81lgELh/cfhF/C5yOCsJx5pk5TJFwl8
3mAlV6KKTcacqEB/kKg+3sY1sv31EdsvpwOcEmXRXhI6hv4yENk0+cEFpEgJ7gkH
gzJ5IYYSEAhfy9lPDOhwTG3VC8Fr/z6gld6V6hym9cv2emd6ifjnP4rivsGg8d77
qs7lw2IbVhzRkVryySXsCXn2O1iu
-----END PRIVATE KEY-----

95
bizmatch/dbschema.ts Normal file
View File

@@ -0,0 +1,95 @@
/* tslint:disable */
/* eslint-disable */
/**
* AUTO-GENERATED FILE - DO NOT EDIT!
*
* This file was automatically generated by pg-to-ts v.4.1.1
* $ pg-to-ts generate -c postgresql://username:password@localhost:5432/bizmatch -t businesses -s public
*
*/
export type Json = unknown;
// Table businesses
export interface Businesses {
id: string;
userId: string | null;
type: number | null;
title: string | null;
description: string | null;
city: string | null;
state: string | null;
price: number | null;
favoritesForUser: string[] | null;
draft: boolean | null;
listingsCategory: string | null;
realEstateIncluded: boolean | null;
leasedLocation: boolean | null;
franchiseResale: boolean | null;
salesRevenue: number | null;
cashFlow: number | null;
supportAndTraining: string | null;
employees: number | null;
established: number | null;
internalListingNumber: number | null;
reasonForSale: string | null;
brokerLicencing: string | null;
internals: string | null;
created: Date | null;
updated: Date | null;
visits: number | null;
lastVisit: Date | null;
}
export interface BusinessesInput {
id?: string;
userId?: string | null;
type?: number | null;
title?: string | null;
description?: string | null;
city?: string | null;
state?: string | null;
price?: number | null;
favoritesForUser?: string[] | null;
draft?: boolean | null;
listingsCategory?: string | null;
realEstateIncluded?: boolean | null;
leasedLocation?: boolean | null;
franchiseResale?: boolean | null;
salesRevenue?: number | null;
cashFlow?: number | null;
supportAndTraining?: string | null;
employees?: number | null;
established?: number | null;
internalListingNumber?: number | null;
reasonForSale?: string | null;
brokerLicencing?: string | null;
internals?: string | null;
created?: Date | null;
updated?: Date | null;
visits?: number | null;
lastVisit?: Date | null;
}
const businesses = {
tableName: 'businesses',
columns: ['id', 'userId', 'type', 'title', 'description', 'city', 'state', 'price', 'favoritesForUser', 'draft', 'listingsCategory', 'realEstateIncluded', 'leasedLocation', 'franchiseResale', 'salesRevenue', 'cashFlow', 'supportAndTraining', 'employees', 'established', 'internalListingNumber', 'reasonForSale', 'brokerLicencing', 'internals', 'created', 'updated', 'visits', 'lastVisit'],
requiredForInsert: [],
primaryKey: 'id',
foreignKeys: { userId: { table: 'users', column: 'id', $type: null as unknown /* users */ }, },
$type: null as unknown as Businesses,
$input: null as unknown as BusinessesInput
} as const;
export interface TableTypes {
businesses: {
select: Businesses;
input: BusinessesInput;
};
}
export const tables = {
businesses,
}

View File

@@ -3,9 +3,10 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve & http-server ../bizmatch-server",
"build": "ng build", "prebuild": "node version.js",
"build.dev": "ng build --configuration dev", "build": "node version.js && ng build",
"build.dev": "node version.js && ng build --configuration dev --output-hashing=all",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs" "serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs"
@@ -33,14 +34,16 @@
"angular-mixed-cdk-drag-drop": "^2.2.3", "angular-mixed-cdk-drag-drop": "^2.2.3",
"browser-bunyan": "^1.8.0", "browser-bunyan": "^1.8.0",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.1",
"dayjs": "^1.11.11",
"express": "^4.18.2", "express": "^4.18.2",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"keycloak-js": "^23.0.7", "keycloak-angular": "^15.2.1",
"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",
"primeflex": "^3.3.1", "primeflex": "^3.3.1",
"primeicons": "^6.0.1", "primeicons": "^6.0.1",
"primeng": "^17.10.0", "primeng": "^17.16.1",
"quill": "^1.3.7", "quill": "^1.3.7",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@@ -55,6 +58,7 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jasmine": "~5.1.4", "@types/jasmine": "~5.1.4",
"@types/node": "^20.11.20", "@types/node": "^20.11.20",
"http-server": "^14.1.1",
"jasmine-core": "~5.1.2", "jasmine-core": "~5.1.2",
"karma": "~6.4.2", "karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",

View File

@@ -2,5 +2,9 @@
"/api": { "/api": {
"target": "http://localhost:3000", "target": "http://localhost:3000",
"secure": false "secure": false
},
"/pictures": {
"target": "http://localhost:8080",
"secure": false
} }
} }

View File

@@ -6,18 +6,26 @@
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
<footer></footer> <footer></footer>
<p-confirmDialog #cd>
<ng-template pTemplate="headless" let-message>
<div class="flex flex-column align-items-center p-5 surface-overlay border-round">
<span class="font-bold text-2xl block mb-2 mt-4">
{{ message.header }}
</span>
<p class="mb-0">{{ message.message }}</p>
<div class="flex align-items-center gap-2 mt-4">
<button pButton label="OK" (click)="cd.accept()" size="small"></button>
</div>
</div>
</ng-template>
</p-confirmDialog>
</div> </div>
<!-- @if (loadingService.isLoading$ | async) { -->
<!-- <div class="progress-spinner flex h-full align-items-center justify-content-center">
<div class="spinner-text">Please wait - we're processing your image...</div>
<p-progressSpinner></p-progressSpinner>
</div> -->
<!-- } -->
@if (loadingService.isLoading$ | async) { @if (loadingService.isLoading$ | async) {
<div class="spinner-overlay"> <div class="spinner-overlay">
<div class="spinner-container"> <div class="spinner-container">
<p-progressSpinner></p-progressSpinner> <p-progressSpinner></p-progressSpinner>
<div class="spinner-text" *ngIf="loadingService.loadingText$ | async as loadingText">{{loadingText}}</div> <div class="spinner-text" *ngIf="loadingService.loadingText$ | async as loadingText">{{ loadingText }}</div>
</div> </div>
</div> </div>
} }

View File

@@ -1,48 +1,67 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, ViewChild } 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 { HeaderComponent } from './components/header/header.component'; import { KeycloakService } from 'keycloak-angular';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { ToastModule } from 'primeng/toast';
import { LoadingService } from './services/loading.service';
import { HomeComponent } from './pages/home/home.component';
import { filter } from 'rxjs/operators';
import { FooterComponent } from './components/footer/footer.component';
import { KeycloakService } from './services/keycloak.service';
import { KeycloakEventType } from './models/keycloak-event';
import { createGenericObject } from './utils/utils';
import onChange from 'on-change'; import onChange from 'on-change';
import { ConfirmationService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { filter } from 'rxjs/operators';
import { ListingCriteria } from '../../../bizmatch-server/src/models/main.model';
import build from '../build';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { LoadingService } from './services/loading.service';
import { UserService } from './services/user.service'; import { UserService } from './services/user.service';
import {ListingCriteria, User} from '../../../common-models/src/main.model' import { createDefaultListingCriteria } from './utils/utils';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, ProgressSpinnerModule, FooterComponent], imports: [CommonModule, RouterOutlet, HeaderComponent, ProgressSpinnerModule, FooterComponent, ConfirmDialogModule],
providers: [ConfirmationService],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss',
}) })
export class AppComponent { export class AppComponent {
build = build;
title = 'bizmatch'; title = 'bizmatch';
actualRoute =''; actualRoute = '';
user:User; listingCriteria: ListingCriteria = onChange(createDefaultListingCriteria(), (path, value, previousValue, applyData) => {
listingCriteria:ListingCriteria = onChange(createGenericObject<ListingCriteria>(),(path, value, previousValue, applyData)=>{ sessionStorage.setItem('criteria', JSON.stringify(value));
sessionStorage.setItem('criteria',JSON.stringify(value));
}); });
public constructor(public loadingService: LoadingService, private router: Router,private activatedRoute: ActivatedRoute, private keycloakService:KeycloakService,private userService:UserService) { public constructor(
this.router.events.pipe( public loadingService: LoadingService,
filter(event => event instanceof NavigationEnd) private router: Router,
).subscribe(() => { private activatedRoute: ActivatedRoute,
private keycloakService: KeycloakService,
private userService: UserService,
private confirmationService: ConfirmationService,
) {
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root; let currentRoute = this.activatedRoute.root;
while (currentRoute.children[0] !== undefined) { while (currentRoute.children[0] !== undefined) {
currentRoute = currentRoute.children[0]; currentRoute = currentRoute.children[0];
} }
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad // Hier haben Sie Zugriff auf den aktuellen Route-Pfad
this.actualRoute=currentRoute.snapshot.url[0].path this.actualRoute = currentRoute.snapshot.url[0].path;
}); });
} }
ngOnInit(){ ngOnInit() {}
this.user = this.userService.getUser(); @HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
this.showVersionDialog();
}
} }
showVersionDialog() {
this.confirmationService.confirm({
target: event.target as EventTarget,
message: `App Version: ${this.build.timestamp}`,
header: 'Version Info',
icon: 'pi pi-info-circle',
accept: () => {},
reject: () => {},
});
}
} }

View File

@@ -1,25 +1,30 @@
import { APP_INITIALIZER, ApplicationConfig, LOCALE_ID, importProvidersFrom } from '@angular/core'; import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
import { routes } from './app.routes'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations'; import { provideAnimations } from '@angular/platform-browser/animations';
import { HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptors, withInterceptorsFromDi } from '@angular/common/http'; import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { SelectOptionsService } from './services/select-options.service'; import { customKeycloakAdapter } from '../keycloak';
import { KeycloakService } from './services/keycloak.service'; import { routes } from './app.routes';
import { UserService } from './services/user.service';
import { LoadingInterceptor } from './interceptors/loading.interceptor'; import { LoadingInterceptor } from './interceptors/loading.interceptor';
import { KeycloakInitializerService } from './services/keycloak-initializer.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()),
{provide:KeycloakService}, { provide: KeycloakService },
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initializeKeycloak, // useFactory: initializeKeycloak,
//useFactory: initializeKeycloak,
useFactory: initializeKeycloak3,
multi: true, multi: true,
deps: [KeycloakService], //deps: [KeycloakService],
deps: [KeycloakInitializerService],
}, },
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
@@ -28,28 +33,58 @@ export const appConfig: ApplicationConfig = {
deps: [SelectOptionsService], deps: [SelectOptionsService],
}, },
{ {
provide:HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass:LoadingInterceptor, useClass: LoadingInterceptor,
multi:true multi: true,
}, },
provideRouter(routes),provideAnimations(), {
// {provide: LOCALE_ID, useValue: 'en-US' } provide: HTTP_INTERCEPTORS,
] useClass: KeycloakBearerInterceptor,
multi: true,
},
provideRouter(
routes,
withEnabledBlockingInitialNavigation(),
withInMemoryScrolling({
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
}),
),
provideAnimations(),
],
}; };
function initUserService(userService:UserService) { function initServices(selectOptions: SelectOptionsService) {
return () => { return async () => {
//selectOptions.init(); await selectOptions.init();
} };
} }
function initServices(selectOptions:SelectOptionsService) { export function initializeKeycloak3(keycloak: KeycloakInitializerService) {
return () => { return () => keycloak.initialize();
selectOptions.init();
}
} }
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 () => return async () => {
keycloak.init({ logger.info(`###>calling keycloakService init ...`);
const authenticated = await keycloak.init({
config: { config: {
url: environment.keycloak.url, url: environment.keycloak.url,
realm: environment.keycloak.realm, realm: environment.keycloak.realm,
@@ -57,7 +92,13 @@ function initializeKeycloak(keycloak: KeycloakService) {
}, },
initOptions: { initOptions: {
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;
}, },
}); });
logger.info(`+++>${authenticated}`);
};
} }

View File

@@ -1,84 +1,132 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { ListingsComponent } from './pages/listings/listings.component';
import { HomeComponent } from './pages/home/home.component';
import { DetailsListingComponent } from './pages/details/details-listing/details-listing.component';
import { AccountComponent } from './pages/subscription/account/account.component';
import { EditListingComponent } from './pages/subscription/edit-listing/edit-listing.component';
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
import { authGuard } from './guards/auth.guard';
import { PricingComponent } from './pages/pricing/pricing.component';
import { LogoutComponent } from './components/logout/logout.component'; import { LogoutComponent } from './components/logout/logout.component';
import { DetailsUserComponent } from './pages/details/details-user/details-user.component'; import { NotFoundComponent } from './components/not-found/not-found.component';
import { AuthGuard } from './guards/auth.guard';
import { ListingCategoryGuard } from './guards/listing-category.guard';
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 { DetailsUserComponent } from './pages/details/details-user/details-user.component';
import { HomeComponent } from './pages/home/home.component';
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
import { PricingComponent } from './pages/pricing/pricing.component';
import { AccountComponent } from './pages/subscription/account/account.component';
import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component';
import { EditCommercialPropertyListingComponent } from './pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component';
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
path: 'listings/:type', path: 'businessListings',
component: ListingsComponent, component: BusinessListingsComponent,
runGuardsAndResolvers: 'always',
}, },
// Umleitung von /listing zu /listing/business
{ {
path: 'listings', path: 'commercialPropertyListings',
pathMatch: 'full', component: CommercialPropertyListingsComponent,
redirectTo: 'listings/business', runGuardsAndResolvers: 'always',
runGuardsAndResolvers:'always' },
{
path: 'brokerListings',
component: BrokerListingsComponent,
runGuardsAndResolvers: 'always',
}, },
{ {
path: 'home', path: 'home',
component: HomeComponent, component: HomeComponent,
}, },
// #########
// Listings Details
{ {
path: 'details-listing/:type/:id', path: 'details-business-listing/:id',
component: DetailsListingComponent, component: DetailsBusinessListingComponent,
}, },
{ {
path: 'details-listing/:type/:id', path: 'details-commercial-property-listing/:id',
component: DetailsListingComponent, component: DetailsCommercialPropertyListingComponent,
}, },
{
path: 'listing/:id',
canActivate: [ListingCategoryGuard],
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
},
// #########
// User Details
{ {
path: 'details-user/:id', path: 'details-user/:id',
component: DetailsUserComponent, component: DetailsUserComponent,
}, },
// #########
// User edit
{
path: 'account',
component: AccountComponent,
canActivate: [AuthGuard],
},
{ {
path: 'account/:id', path: 'account/:id',
component: AccountComponent, component: AccountComponent,
canActivate: [authGuard], canActivate: [AuthGuard],
},
// #########
// Create, Update Listings
{
path: 'editBusinessListing/:id',
component: EditBusinessListingComponent,
canActivate: [AuthGuard],
}, },
{ {
path: 'editListing/:id', path: 'createBusinessListing',
component: EditListingComponent, component: EditBusinessListingComponent,
canActivate: [authGuard], canActivate: [AuthGuard],
}, },
{ {
path: 'createListing', path: 'editCommercialPropertyListing/:id',
component: EditListingComponent, component: EditCommercialPropertyListingComponent,
canActivate: [authGuard], canActivate: [AuthGuard],
}, },
{
path: 'createCommercialPropertyListing',
component: EditCommercialPropertyListingComponent,
canActivate: [AuthGuard],
},
// #########
// My Listings
{ {
path: 'myListings', path: 'myListings',
component: MyListingComponent, component: MyListingComponent,
canActivate: [authGuard], canActivate: [AuthGuard],
}, },
// #########
// My Favorites
{ {
path: 'myFavorites', path: 'myFavorites',
component: FavoritesComponent, component: FavoritesComponent,
canActivate: [authGuard], canActivate: [AuthGuard],
}, },
// #########
// EMAil Us
{ {
path: 'emailUs', path: 'emailUs',
component: EmailUsComponent, component: EmailUsComponent,
canActivate: [authGuard], canActivate: [AuthGuard],
}, },
// #########
// Logout
{ {
path: 'logout', path: 'logout',
component: LogoutComponent, component: LogoutComponent,
canActivate: [authGuard], canActivate: [AuthGuard],
}, },
// #########
// Pricing
{ {
path: 'pricing', path: 'pricing',
component: PricingComponent component: PricingComponent,
}, },
{ path: '**', redirectTo: 'home' }, { path: '**', redirectTo: 'home' },
]; ];

View File

@@ -1,26 +1,502 @@
<div class="surface-0 px-4 py-4 md:px-6 lg:px-8"> <div class="surface-0 px-4 py-4 md:px-6 lg:px-8">
<div class="surface-0"> <div class="surface-0">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-3 md:mb-0 mb-3"> <div class="col-12 md:col-3 md:mb-0 mb-3 cursor-pointer" routerLink="/home">
<img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3"> <img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3" />
<div class="text-500">© 2024 Bizmatch All rights reserved.</div> <div class="text-500">© 2024 Bizmatch All rights reserved.</div>
</div> </div>
<div class="col-12 md:col-3"> <div class="col-12 md:col-3">
<div class="text-black mb-4 flex flex-wrap" style="max-width: 290px">BizMatch, Inc., 1001 Blucher Street, Corpus Christi, Texas 78401</div> <div class="text-black mb-4 flex flex-wrap" style="max-width: 290px">BizMatch, Inc., 1001 Blucher Street, Corpus Christi, Texas 78401</div>
<div class="text-black mb-3"><i class="text-white pi pi-phone surface-800 border-round p-1 mr-2"></i>1-800-840-6025</div> <div class="text-black mb-3"><i class="text-white pi pi-phone surface-800 border-round p-1 mr-2"></i>1-800-840-6025</div>
<div class="text-black mb-3"><i class="text-white pi pi-inbox surface-800 border-round p-1 mr-2"></i>bizmatch&#64;biz-match.com</div> <div class="text-black mb-3"><i class="text-white pi pi-inbox surface-800 border-round p-1 mr-2"></i>info&#64;bizmatch.net</div>
</div> </div>
<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">Legal</div> <div class="text-black font-bold line-height-3 mb-3">Legal</div>
<a class="line-height-3 block cursor-pointer mb-2">Terms of use</a> <a class="line-height-3 block cursor-pointer mb-2" (click)="termsVisible = true">Terms of use</a>
<a class="line-height-3 block cursor-pointer mb-2">Privacy statement</a> <a class="line-height-3 block cursor-pointer mb-2" (click)="privacyVisible = true">Privacy statement</a>
</div> </div>
<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="!userService.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="userService.isLoggedIn()" [routerLink]="['/account',userService.getUser()?.id]" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a> <a *ngIf="!keycloakService.isLoggedIn()" (click)="register()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Register</a>
<a *ngIf="userService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="userService.logout()">Log Out</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>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<p-sidebar [(visible)]="privacyVisible" styleClass="w-30rem" position="right">
<ng-template pTemplate="header">
<span class="font-semibold text-xl">Privacy Statement</span>
</ng-template>
<section id="content" role="main">
<article id="post-2" class="post-2 page type-page status-publish hentry pmpro-has-access">
<section class="entry-content">
<div class="container" style="padding: 3.5% 0 3.75% 0 !important">
<p>
<strong>Privacy Policy</strong><br />
We are committed to protecting your privacy. We have established this statement as a testament to our commitment to your privacy.
</p>
<p>This Privacy Policy relates to the use of any personal information you provide to us through this websites.</p>
<p>
By accepting the Privacy Policy during registration or the sending of an enquiry, you expressly consent to our collection, storage, use and disclosure of your personal information as described in this Privacy
Policy.
</p>
<p>
We may update our Privacy Policy from time to time. Our Privacy Policy was last updated in Febuary 2018 and is effective upon acceptance for new users. By continuing to use our websites or otherwise
continuing to deal with us, you accept this Privacy Policy.
</p>
<p>
<strong>Collection of personal information</strong><br />
Anyone can browse our websites without revealing any personally identifiable information.
</p>
<p>However, should you wish to contact a business for sale, a franchise opportunity or an intermediary, we will require you to provide some personal information.</p>
<p>Should you wish to advertise your services, your business (es) or your franchise opportunity, we will require you to provide some personal information.</p>
<p>By providing personal information, you are consenting to the transfer and storage of that information on our servers located in the United States.</p>
<p>We may collect and store the following personal information:</p>
<p>
Your name, email address, physical address, telephone numbers, and (depending on the service used), your business information, financial information, such as credit / payment card details;<br />
transactional information based on your activities on the site; information that you disclose in a forum on any of our websites, feedback, correspondence through our websites, and correspondence sent to
us;<br />
other information from your interaction with our websites, services, content and advertising, including computer and connection information, statistics on page views, traffic to and from the sites, ad data,
IP address and standard web log information;<br />
supplemental information from third parties (for example, if you incur a debt, we will generally conduct a credit check by obtaining additional information about you from a credit bureau, as permitted by law;
or if the information you provide cannot be verified,<br />
we may ask you to send us additional information, or to answer additional questions online to help verify your information).
</p>
<p>
<strong>How we use your information</strong><br />
The primary reason we collect your personal information is to improve the services we deliver to you through our website. By registering or sending an enquiry through our website, you agree that we may use
your personal information to:<br />
provide the services and customer support you request;<br />
connect you with relevant parties:<br />
If you are a buyer we will pass some or all of your details on to the seller / intermediary along with any message you have typed. This allows the seller to contact you in order to pursue a possible sale of a
business;<br />
If you are a seller / intermediary, we will disclose your details where you have given us permission to do so;<br />
resolve disputes, collect fees, and troubleshoot problems;<br />
prevent potentially prohibited or illegal activities, and enforce our Terms and Conditions;<br />
customize, measure and improve our services, conduct internal market research, provide content and advertising;<br />
tell you about other Biz-Match products and services, target marketing, send you service updates, and promotional offers based on your communication preferences.
</p>
<p>
<strong>Our disclosure of your information</strong><br />
We may disclose personal information to respond to legal requirements, enforce our policies, respond to claims that a listing or other content infringes the rights of others, or protect anyones rights,
property, or safety.
</p>
<p>
We may also share your personal information with<br />
When you select to register an account as a business buyer, you provide your personal details and we will pass this on to a seller of a business or franchise when you request more information.
</p>
<p>
When you select to register an account as a business broker or seller on the site, we provide a public platform on which to establish your business profile. This profile consists of pertinent facts about your
business along with your personal information; namely, the contact information you provide to facilitate contact between you and other users of the site. Direct email addresses and telephone numbers will not
be publicly displayed unless you specifically include it on your profile.
</p>
<p>
The information a user includes within the forums provided on the site is publicly available to other users of the site. Please be aware that any personal information you elect to provide in a public forum
may be used to send you unsolicited messages; we are not responsible for the personal information a user elects to disclose within their public profile, or in the private communications that users engage in
on the site.
</p>
<p>
We post testimonials on the site obtained from users. These testimonials may include the name, city, state or region and business of the user. We obtain permission from our users prior to posting their
testimonials on the site. We are not responsible for any personal information a user selects to include within their testimonial.
</p>
<p>
When you elect to email a friend about the site, or a particular business, we request the third partys email address to send this one time email. We do not share this information with any third parties for
their promotional purposes and only store the information to gauge the effectiveness of our referral program.
</p>
<p>We may share your personal information with our service providers where necessary. We employ the services of a payment processor to fulfil payment for services purchased on the site.</p>
<p>
We works with a number of partners or affiliates, where we provide marketing services for these companies. These third party agents collect your personal information to facilitate your service request and the
information submitted here is governed by their privacy policy.
</p>
<p>
<strong>Masking Policy</strong><br />
In some cases, where the third party agent collects your information, the affiliate portal may appear within a BizMatch.net frame. It is presented as a BizMatch.net page for a streamlined user interface
however the data collected on such pages is governed by the third party agents privacy policy.
</p>
<p>
<strong>Legal Disclosure</strong><br />
In certain circumstances, we may be legally required to disclose information collected on the site to law enforcement, government agencies or other third parties. We reserve the right to disclose information
to our service providers and to law enforcement or government agencies where a formal request such as in response to a court order, subpoena or judicial proceeding is made. Where we believe in good faith that
disclosure of information is necessary to prevent imminent physical or financial harm, or loss, or in protecting against illegal activity on the site, we reserve to disclose information.
</p>
<p>
Should the company undergo the merger, acquisition or sale of some or all of its assets, your personal information may likely be a part of the transferred assets. In such an event, your personal information
on the site, would be governed by this privacy statement; any changes to the privacy practices governing your information as a result of transfer would be relayed to you by means of a prominent notice on the
Site, or by email.
</p>
<p>
<strong>Using information from BizMatch.net website</strong><br />
In certain cases, (where you are receiving contact details of buyers interested in your business opportunity or a business opportunity you represent), you must comply with data protection laws, and give other
users a chance to remove themselves from your database and a chance to review what information you have collected about them.
</p>
<p>
<strong>You agree to use BizMatch.net user information only for:</strong>
</p>
<p>
BizMatch.net transaction-related purposes that are not unsolicited commercial messages;<br />
using services offered through BizMatch.net, or<br />
other purposes that a user expressly chooses.
</p>
<p>
<strong>Marketing</strong><br />
We do not sell or rent your personal information to third parties for their marketing purposes without your explicit consent. Where you explicitly express your consent at the point of collection to receive
offers from third party partners or affiliates, we will communicate to you on their behalf. We will not pass your information on.
</p>
<p>
You will receive email marketing communications from us throughout the duration of your relationship with our websites. If you do not wish to receive marketing communications from us you may unsubscribe and /
or change your preferences at any time by following instructions included within a communication or emailing Customer Services.
</p>
<p>If you have an account with one of our websites you can also log in and click the email preferences link to unsubscribe and / or change your preferences.</p>
<p>
Please note that we reserve the right to send all website users notifications and administrative emails where necessary which are considered a part of the service. Given that these messages arent promotional
in nature, you will be unable to opt-out of them.
</p>
<p>
<strong>Cookies</strong><br />
A cookie is a small text file written to your hard drive that contains information about you. Cookies do not contain any personal information about users. Once you close your browser or log out of the
website, the cookie simply terminates. We use cookies so that we can personalise your experience of our websites.
</p>
<p>
If you set up your browser to reject the cookie, you may still use the website however; doing so may interfere with your use of some aspects of our websites. Some of our business partners use cookies on our
site (for example, advertisers). We have no access to or control over these cookies.
</p>
<p>For more information about how BizMatch.net uses cookies please read our Cookie Policy.</p>
<p>
<strong>Spam, spyware or spoofing</strong><br />
We and our users do not tolerate spam. Make sure to set your email preferences so we can communicate with you, as you prefer. Please add us to your safe senders list. To report spam or spoof emails, please
contact us using the contact information provided in the Contact Us section of this privacy statement.
</p>
<p>
You may not use our communication tools to send spam or otherwise send content that would breach our Terms and Conditions. We automatically scan and may manually filter messages to check for spam, viruses,
phishing attacks and other malicious activity or illegal or prohibited content. We may also store these messages for back up purposes only.
</p>
<p>
If you send an email to an email address that is not registered in our community, we do not permanently store that email or use that email address for any marketing purpose. We do not rent or sell these email
addresses.
</p>
<p>
<strong>Account protection</strong><br />
Your password is the key to your account. Make sure this is stored safely. Use unique numbers, letters and special characters, and do not disclose your password to anyone. If you do share your password or
your personal information with others, remember that you are responsible for all actions taken in the name of your account. If you lose control of your password, you may lose substantial control over your
personal information and may be subject to legally binding actions taken on your behalf. Therefore, if your password has been compromised for any reason, you should immediately notify us and change your
password.
</p>
<p>
<strong>Accessing, reviewing and changing your personal information</strong><br />
You can view and amend your personal information at any time by logging in to your account online. You must promptly update your personal information if it changes or is inaccurate.
</p>
<p>If at any time you wish to close your account, please contact Customer Services and instruct us to do so. We will process your request as soon as we can.</p>
<p>You may also contact us at any time to find out what information we hold about you, what we do with it and ask us to update it for you.</p>
<p>
We do retain personal information from closed accounts to comply with law, prevent fraud, collect any fees owed, resolve disputes, troubleshoot problems, assist with any investigations, enforce our Terms and
Conditions, and take other actions otherwise permitted by law.
</p>
<p>
<strong>Security</strong><br />
Your information is stored on our servers located in the USA. We treat data as an asset that must be protected and use a variety of tools (encryption, passwords, physical security, etc.) to protect your
personal information against unauthorized access and disclosure. However, no method of security is 100% effective and while we take every measure to protect your personal information, we make no guarantees of
its absolute security.
</p>
<p>We employ the use of SSL encryption during the transmission of sensitive data across our websites.</p>
<p>
<strong>Third parties</strong><br />
Except as otherwise expressly included in this Privacy Policy, this document addresses only the use and disclosure of information we collect from you. If you disclose your information to others, whether they
are buyers or sellers on our websites or other sites throughout the internet, different rules may apply to their use or disclosure of the information you disclose to them. Dynamis does not control the privacy
policies of third parties, and you are subject to the privacy policies of those third parties where applicable.
</p>
<p>We encourage you to ask questions before you disclose your personal information to others.</p>
<p>
<strong>General</strong><br />
We may change this Privacy Policy from time to time as we add new products and applications, as we improve our current offerings, and as technologies and laws change. You can determine when this Privacy
Policy was last revised by referring to the “Last Updated” legend at the top of this page.
</p>
<p>
Any changes will become effective upon our posting of the revised Privacy Policy on our affected websites. We will provide notice to you if these changes are material and, where required by applicable law, we
will obtain your consent. This notice may be provided by email, by posting notice of the changes on our affected websites or by other means, consistent with applicable laws.
</p>
<p>
<strong>Contact Us</strong><br />
If you have any questions or comments about our privacy policy, and you cant find the answer to your question on our help pages, please contact us using this form or email support&#64;bizmatch.net, or write
to us at BizMatch, 715 S. Tanahua, Corpus Christi, TX 78401.)
</p>
</div>
</section>
</article>
</section>
</p-sidebar>
<p-sidebar [(visible)]="termsVisible" styleClass="w-30rem" position="right">
<ng-template pTemplate="header">
<span class="font-semibold text-xl">Terms of use</span>
</ng-template>
<section id="content" role="main">
<article id="post-1" class="post-1 page type-page status-publish hentry pmpro-has-access">
<section class="entry-content">
<div class="container" style="padding: 3.5% 0 3.75% 0 !important">
<b><span>AGREEMENT BETWEEN USER AND BizMatch</span></b
><span
><p></p>
<p><span>The BizMatch Web Site is comprised of various Web pages operated by BizMatch.</span><span></span></p>
<p>
<span
>The BizMatch Web Site is offered to you conditioned on your acceptance without modification of the terms, conditions, and notices contained herein. Your use of the BizMatch Web Site constitutes your
agreement to all such terms, conditions, and notices.</span
><span></span>
</p>
<p>
<b><span>MODIFICATION OF THESE TERMS OF USE</span></b
><span></span>
</p>
<p>
<span
>BizMatch reserves the right to change the terms, conditions, and notices under which the BizMatch Web Site is offered, including but not limited to the charges associated with the use of the BizMatch Web
Site.</span
><span></span>
</p>
<p>
<b><span>LINKS TO THIRD PARTY SITES</span></b
><span></span>
</p>
<p>
<span
>The BizMatch Web Site may contain links to other Web Sites ("Linked Sites"). The Linked Sites are not under the control of BizMatch and BizMatch is not responsible for the contents of any Linked Site,
including without limitation any link contained in a Linked Site, or any changes or updates to a Linked Site. BizMatch is not responsible for webcasting or any other form of transmission received from any
Linked Site. BizMatch is providing these links to you only as a convenience, and the inclusion of any link does not imply endorsement by BizMatch of the site or any association with its operators.</span
><span></span>
</p>
<p>
<b><span>NO UNLAWFUL OR PROHIBITED USE</span></b
><span></span>
</p>
<p>
<span
>As a condition of your use of the BizMatch Web Site, you warrant to BizMatch that you will not use the BizMatch Web Site for any purpose that is unlawful or prohibited by these terms, conditions, and
notices. You may not use the BizMatch Web Site in any manner which could damage, disable, overburden, or impair the BizMatch Web Site or interfere with any other partys use and enjoyment of the BizMatch
Web Site. You may not obtain or attempt to obtain any materials or information through any means not intentionally made available or provided for through the BizMatch Web Sites.</span
><span></span>
</p>
<p>
<b><span>USE OF COMMUNICATION SERVICES</span></b
><span></span>
</p>
<p>
<span
>The BizMatch Web Site may contain bulletin board services, chat areas, news groups, forums, communities, personal web pages, calendars, and/or other message or communication facilities designed to enable
you to communicate with the public at large or with a group (collectively, "Communication Services"), you agree to use the Communication Services only to post, send and receive messages and material that
are proper and related to the particular Communication Service. By way of example, and not as a limitation, you agree that when using a Communication Service, you will not:</span
><span></span>
</p>
<p>&nbsp;</p>
<p class="MsoNormal"><!--[if !supportLists]--></p>
<p>
<span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Defame, abuse, harass, stalk, threaten or otherwise violate the legal rights (such as rights of privacy and publicity) of others.</span><span></span>
</p>
<p>&nbsp;</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Publish, post, upload, distribute or disseminate any inappropriate, profane, defamatory, infringing, obscene, indecent or unlawful topic, name, material or information.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span
>Upload files that contain software or other material protected by intellectual property laws (or by rights of privacy of publicity) unless you own or control the rights thereto or have received all
necessary consents.</span
>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Upload files that contain viruses, corrupted files, or any other similar software or programs that may damage the operation of anothers computer.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Advertise or offer to sell or buy any goods or services for any business purpose, unless such Communication Service specifically allows such messages.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Conduct or forward surveys, contests, pyramid schemes or chain letters.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Download any file posted by another user of a Communication Service that you know, or reasonably should know, cannot be legally distributed in such manner.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span
>Falsify or delete any author attributions, legal or other proper notices or proprietary designations or labels of the origin or source of software or other material contained in a file that is
uploaded.</span
>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Restrict or inhibit any other user from using and enjoying the Communication Services.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Violate any code of conduct or other guidelines which may be applicable for any particular Communication Service.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Harvest or otherwise collect information about others, including e-mail addresses, without their consent.</span>
</p>
<p class="MsoNormal">
<!--[if !supportLists]--><span
><span>§<span>&nbsp; </span></span></span
><!--[endif]--><span>Violate any applicable laws or regulations.</span>
</p>
<p class="MsoNormal">
<span
>BizMatch has no obligation to monitor the Communication Services. However, BizMatch reserves the right to review materials posted to a Communication Service and to remove any materials in its sole
discretion. BizMatch reserves the right to terminate your access to any or all of the Communication Services at any time without notice for any reason whatsoever.</span
><span></span>
</p>
<p>
<span
>BizMatch reserves the right at all times to disclose any information as necessary to satisfy any applicable law, regulation, legal process or governmental request, or to edit, refuse to post or to remove
any information or materials, in whole or in part, in BizMatchs sole discretion.</span
><span></span>
</p>
<p>
<span
>Always use caution when giving out any personally identifying information about yourself or your children in any Communication Service. BizMatch does not control or endorse the content, messages or
information found in any Communication Service and, therefore, BizMatch specifically disclaims any liability with regard to the Communication Services and any actions resulting from your participation in
any Communication Service. Managers and hosts are not authorized BizMatch spokespersons, and their views do not necessarily reflect those of BizMatch.</span
><span></span>
</p>
<p>
<span
>Materials uploaded to a Communication Service may be subject to posted limitations on usage, reproduction and/or dissemination. You are responsible for adhering to such limitations if you download the
materials.</span
><span></span>
</p>
<p>
<b><span>MATERIALS PROVIDED TO BizMatch OR POSTED AT ANY BizMatch WEB SITE</span></b
><span></span>
</p>
<p>
<span
>BizMatch does not claim ownership of the materials you provide to BizMatch (including feedback and suggestions) or post, upload, input or submit to any BizMatch Web Site or its associated services
(collectively "Submissions"). However, by posting, uploading, inputting, providing or submitting your Submission you are granting BizMatch, its affiliated companies and necessary sublicensees permission
to use your Submission in connection with the operation of their Internet businesses including, without limitation, the rights to: copy, distribute, transmit, publicly display, publicly perform,
reproduce, edit, translate and reformat your Submission; and to publish your name in connection with your Submission.</span
><span></span>
</p>
<p>
<span
>No compensation will be paid with respect to the use of your Submission, as provided herein. BizMatch is under no obligation to post or use any Submission you may provide and may remove any Submission at
any time in BizMatchs sole discretion.</span
><span></span>
</p>
<p>
<span
>By posting, uploading, inputting, providing or submitting your Submission you warrant and represent that you own or otherwise control all of the rights to your Submission as described in this section
including, without limitation, all the rights necessary for you to provide, post, upload, input or submit the Submissions.</span
><span></span>
</p>
<p>
<b><span>LIABILITY DISCLAIMER</span></b
><span></span>
</p>
<p>
<span
>THE INFORMATION, SOFTWARE, PRODUCTS, AND SERVICES INCLUDED IN OR AVAILABLE THROUGH THE BizMatch WEB SITE MAY INCLUDE INACCURACIES OR TYPOGRAPHICAL ERRORS. CHANGES ARE PERIODICALLY ADDED TO THE
INFORMATION HEREIN. BizMatch AND/OR ITS SUPPLIERS MAY MAKE IMPROVEMENTS AND/OR CHANGES IN THE BizMatch WEB SITE AT ANY TIME. ADVICE RECEIVED VIA THE BizMatch WEB SITE SHOULD NOT BE RELIED UPON FOR
PERSONAL, MEDICAL, LEGAL OR FINANCIAL DECISIONS AND YOU SHOULD CONSULT AN APPROPRIATE PROFESSIONAL FOR SPECIFIC ADVICE TAILORED TO YOUR SITUATION.</span
><span></span>
</p>
<p>
<span
>BizMatch AND/OR ITS SUPPLIERS MAKE NO REPRESENTATIONS ABOUT THE SUITABILITY, RELIABILITY, AVAILABILITY, TIMELINESS, AND ACCURACY OF THE INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS
CONTAINED ON THE BizMatch WEB SITE FOR ANY PURPOSE. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, ALL SUCH INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS ARE PROVIDED "AS IS" WITHOUT
WARRANTY OR CONDITION OF ANY KIND. BizMatch AND/OR ITS SUPPLIERS HEREBY DISCLAIM ALL WARRANTIES AND CONDITIONS WITH REGARD TO THIS INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS, INCLUDING
ALL IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT.</span
><span></span>
</p>
<p>
<span
>TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL BizMatch AND/OR ITS SUPPLIERS BE LIABLE FOR ANY DIRECT, INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL, CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF USE, DATA OR PROFITS, ARISING OUT OF OR IN ANY WAY CONNECTED WITH THE USE OR PERFORMANCE OF THE BizMatch WEB SITE, WITH THE DELAY OR INABILITY
TO USE THE BizMatch WEB SITE OR RELATED SERVICES, THE PROVISION OF OR FAILURE TO PROVIDE SERVICES, OR FOR ANY INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS OBTAINED THROUGH THE BizMatch
WEB SITE, OR OTHERWISE ARISING OUT OF THE USE OF THE BizMatch WEB SITE, WHETHER BASED ON CONTRACT, TORT, NEGLIGENCE, STRICT LIABILITY OR OTHERWISE, EVEN IF BizMatch OR ANY OF ITS SUPPLIERS HAS BEEN
ADVISED OF THE POSSIBILITY OF DAMAGES. BECAUSE SOME STATES/JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY
TO YOU. IF YOU ARE DISSATISFIED WITH ANY PORTION OF THE BizMatch WEB SITE, OR WITH ANY OF THESE TERMS OF USE, YOUR SOLE AND EXCLUSIVE REMEDY IS TO DISCONTINUE USING THE BizMatch WEB SITE.</span
><span></span>
</p>
<p><span>SERVICE CONTACT : info&#64;bizmatch.net</span><span></span></p>
<p>
<b><span>TERMINATION/ACCESS RESTRICTION</span></b
><span></span>
</p>
<p>
<span
>BizMatch reserves the right, in its sole discretion, to terminate your access to the BizMatch Web Site and the related services or any portion thereof at any time, without notice. GENERAL To the maximum
extent permitted by law, this agreement is governed by the laws of the State of Washington, U.S.A. and you hereby consent to the exclusive jurisdiction and venue of courts in King County, Washington,
U.S.A. in all disputes arising out of or relating to the use of the BizMatch Web Site. Use of the BizMatch Web Site is unauthorized in any jurisdiction that does not give effect to all provisions of these
terms and conditions, including without limitation this paragraph. You agree that no joint venture, partnership, employment, or agency relationship exists between you and BizMatch as a result of this
agreement or use of the BizMatch Web Site. BizMatchs performance of this agreement is subject to existing laws and legal process, and nothing contained in this agreement is in derogation of BizMatchs
right to comply with governmental, court and law enforcement requests or requirements relating to your use of the BizMatch Web Site or information provided to or gathered by BizMatch with respect to such
use. If any part of this agreement is determined to be invalid or unenforceable pursuant to applicable law including, but not limited to, the warranty disclaimers and liability limitations set forth
above, then the invalid or unenforceable provision will be deemed superseded by a valid, enforceable provision that most closely matches the intent of the original provision and the remainder of the
agreement shall continue in effect. Unless otherwise specified herein, this agreement constitutes the entire agreement between the user and BizMatch with respect to the BizMatch Web Site and it supersedes
all prior or contemporaneous communications and proposals, whether electronic, oral or written, between the user and BizMatch with respect to the BizMatch Web Site. A printed version of this agreement and
of any notice given in electronic form shall be admissible in judicial or administrative proceedings based upon or relating to this agreement to the same extent an d subject to the same conditions as
other business documents and records originally generated and maintained in printed form. It is the express wish to the parties that this agreement and all related documents be drawn up in English.</span
><span></span>
</p>
<p>
<b><span>COPYRIGHT AND TRADEMARK NOTICES:</span></b
><span></span>
</p>
<p><span>All contents of the BizMatch Web Site are: Copyright 2011 by Bizmatch Business Solutions and/or its suppliers. All rights reserved.</span><span></span></p>
<p>
<b><span>TRADEMARKS</span></b
><span></span>
</p>
<p><span>The names of actual companies and products mentioned herein may be the trademarks of their respective owners.</span><span></span></p>
<p>
<span
>The example companies, organizations, products, people and events depicted herein are fictitious. No association with any real company, organization, product, person, or event is intended or should be
inferred.</span
><span></span>
</p>
<p><span>Any rights not expressly granted herein are reserved.</span><span></span></p>
<p>
<b><span>NOTICES AND PROCEDURE FOR MAKING CLAIMS OF COPYRIGHT INFRINGEMENT</span></b
><span></span>
</p>
<p>
<span
>Pursuant to Title 17, United States Code, Section 512(c)(2), notifications of claimed copyright infringement under United States copyright law should be sent to Service Providers Designated Agent. ALL
INQUIRIES NOT RELEVANT TO THE FOLLOWING PROCEDURE WILL RECEIVE NO RESPONSE. See Notice and Procedure for Making Claims of Copyright Infringement.</span
><span
><br />
<!--[if !supportLineBreakNewLine]--></span
>
</p>
<p class="MsoNormal">&nbsp;</p>
<p class="MsoNormal">
We reserve the right to update or revise these Terms of Use at any time without&nbsp;notice. Please check the Terms of Use periodically for changes. The revised&nbsp;terms will be effective immediately as
soon as they are posted on the WebSite&nbsp;and by continuing to use the Site you agree to be bound by the revised terms<span
><br />
<!--[endif]--></span
>
</p></span
>
</div>
</section>
</article>
</section>
</p-sidebar>

View File

@@ -1,25 +1,24 @@
import { CommonModule } from '@angular/common';
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 { SidebarModule } from 'primeng/sidebar';
import { InputTextModule } from 'primeng/inputtext';
import {StyleClassModule} from 'primeng/styleclass';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { UserService } from '../../services/user.service';
import { SharedModule } from '../../shared/shared/shared.module'; import { SharedModule } from '../../shared/shared/shared.module';
@Component({ @Component({
selector: 'footer', selector: 'footer',
standalone: true, standalone: true,
imports: [SharedModule], imports: [SharedModule, SidebarModule],
templateUrl: './footer.component.html', templateUrl: './footer.component.html',
styleUrl: './footer.component.scss' styleUrl: './footer.component.scss',
}) })
export class FooterComponent { export class FooterComponent {
constructor(public userService:UserService){} privacyVisible = false;
login(){ termsVisible = false;
this.userService.login(window.location.href); constructor(public keycloakService: KeycloakService) {}
login() {
this.keycloakService.login({
redirectUri: window.location.href,
});
}
register() {
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
} }
} }

View File

@@ -1,11 +1,12 @@
<div class="wrapper"> <div class="wrapper">
<div class="pl-3 flex align-items-center gap-2"> <div class="pl-3 flex align-items-center gap-2">
<a routerLink="/home"><img src="assets/images/header-logo.png" height="40" alt="bizmatch" /></a> <a routerLink="/home"><img src="assets/images/header-logo.png" height="40" alt="bizmatch" /></a>
<p-tabMenu [model]="tabItems" ariaLabelledBy="label" styleClass="flex" [activeItem]="activeItem"> <p-tabMenu [model]="tabItems" ariaLabelledBy="label" styleClass="flex" [activeItem]="activeItem"> </p-tabMenu>
</p-tabMenu>
<p-menubar [model]="menuItems"></p-menubar> <p-menubar [model]="menuItems"></p-menubar>
<div *ngIf="user$ | async as user else empty">Welcome, {{user.firstname}}</div> <p-menubar [model]="loginItems"></p-menubar>
<ng-template #empty> @if(user){
</ng-template> <div>Welcome, {{ user.firstName }}</div>
}
<ng-template #empty> </ng-template>
</div> </div>
</div> </div>

View File

@@ -1,40 +1,41 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router';
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';
import { OverlayPanelModule } from 'primeng/overlaypanel'; import { OverlayPanelModule } from 'primeng/overlaypanel';
import { environment } from '../../../environments/environment';
import { UserService } from '../../services/user.service';
import { TabMenuModule } from 'primeng/tabmenu'; import { TabMenuModule } from 'primeng/tabmenu';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { faUserGear } from '@fortawesome/free-solid-svg-icons'; import { KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
import { Router } from '@angular/router'; import { environment } from '../../../environments/environment';
import { User } from '../../../../../common-models/src/main.model'; import { map2User } from '../../utils/utils';
@Component({ @Component({
selector: 'header', selector: 'header',
standalone: true, standalone: true,
imports: [CommonModule, MenubarModule, ButtonModule, OverlayPanelModule, TabMenuModule ], imports: [CommonModule, MenubarModule, ButtonModule, OverlayPanelModule, TabMenuModule],
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrl: './header.component.scss' styleUrl: './header.component.scss',
}) })
export class HeaderComponent { export class HeaderComponent {
public buildVersion = environment.buildVersion; public buildVersion = environment.buildVersion;
user$:Observable<User> user$: Observable<KeycloakUser>;
user:User; user: KeycloakUser;
public tabItems: MenuItem[]; public tabItems: MenuItem[];
public loginItems: MenuItem[];
public menuItems: MenuItem[]; public menuItems: MenuItem[];
activeItem activeItem;
faUserGear=faUserGear faUserGear = faUserGear;
constructor(public userService: UserService,private router: Router) { constructor(public keycloakService: KeycloakService, private router: Router) {}
} async ngOnInit() {
const token = await this.keycloakService.getToken();
ngOnInit(){ this.user = map2User(token);
this.user$=this.userService.getUserObservable(); //this.user$ = this.keycloakService
this.user$.subscribe(u=>{ // this.user$.subscribe(u => {
this.user=u; // this.user = u;
this.menuItems = [ this.menuItems = [
{ {
label: 'User Actions', label: 'User Actions',
@@ -43,77 +44,90 @@ export class HeaderComponent {
{ {
label: 'Account', label: 'Account',
icon: 'pi pi-user', icon: 'pi pi-user',
routerLink: `/account/${this.user.id}`, routerLink: `/account`,
visible: this.isUserLogedIn() visible: this.keycloakService.isLoggedIn(),
}, },
{ {
label: 'Create Listing', label: 'Create Listing',
icon: 'pi pi-plus-circle', icon: 'pi pi-plus-circle',
routerLink: "/createListing", routerLink: '/createBusinessListing',
visible: this.isUserLogedIn() visible: this.keycloakService.isLoggedIn(),
}, },
{ {
label: 'My Listings', label: 'My Listings',
icon: 'pi pi-list', icon: 'pi pi-list',
routerLink:"/myListings", routerLink: '/myListings',
visible: this.isUserLogedIn() visible: this.keycloakService.isLoggedIn(),
}, },
{ {
label: 'My Favorites', label: 'My Favorites',
icon: 'pi pi-star', icon: 'pi pi-star',
routerLink:"/myFavorites", routerLink: '/myFavorites',
visible: this.isUserLogedIn() visible: this.keycloakService.isLoggedIn(),
}, },
{ {
label: 'EMail Us', label: 'EMail Us',
icon: 'fa-regular fa-envelope', icon: 'fa-regular fa-envelope',
routerLink:"/emailUs", routerLink: '/emailUs',
visible: this.isUserLogedIn() visible: this.keycloakService.isLoggedIn(),
}, },
{ {
label: 'Logout', label: 'Logout',
icon: 'fa-solid fa-right-from-bracket', icon: 'fa-solid fa-right-from-bracket',
routerLink:"/logout", routerLink: '/logout',
visible: this.isUserLogedIn() visible: this.keycloakService.isLoggedIn(),
}, },
{ {
label: 'Login', label: 'Login',
icon: 'fa-solid fa-right-from-bracket', icon: 'fa-solid fa-right-from-bracket',
//routerLink:"/account",
command: () => this.login(), command: () => this.login(),
visible: !this.isUserLogedIn() visible: !this.keycloakService.isLoggedIn(),
}, },
] ],
} },
] ];
}); // });
this.tabItems = [ this.tabItems = [
{ {
label: 'Businesses for Sale', label: 'Businesses for Sale',
routerLink: '/listings/business', routerLink: '/businessListings',
fragment:'' state: {},
},
{
label: 'Professionals/Brokers Directory',
routerLink: '/listings/professionals_brokers',
fragment:''
}, },
{ {
label: 'Commercial Property', label: 'Commercial Property',
routerLink: '/listings/commercialProperty', routerLink: '/commercialPropertyListings',
fragment:'' state: {},
} },
{
label: 'Professionals/Brokers Directory',
routerLink: '/brokerListings',
state: {},
},
]; ];
this.loginItems = [
this.activeItem=this.tabItems[0]; {
label: 'Login',
command: () => this.login(),
visible: !this.keycloakService.isLoggedIn(),
},
{
label: 'Register',
command: () => this.register(),
visible: !this.keycloakService.isLoggedIn(),
},
];
this.activeItem = this.tabItems[0];
} }
navigateWithState(dest: string, state: any) { navigateWithState(dest: string, state: any) {
this.router.navigate([dest], { state: state }); this.router.navigate([dest], { state: state });
} }
isUserLogedIn(){ login() {
return this.userService?.isLoggedIn(); this.keycloakService.login({
redirectUri: window.location.href,
});
} }
login(){ register() {
this.userService.login(window.location.href); this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
} }
} }

View File

@@ -2,14 +2,13 @@
<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> <div class="flex justify-content-between">
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button> <p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true" size="small" class="mr-2"></p-button>
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button> <p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true" size="small"></p-button>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,4 @@
::ng-deep p-selectbutton.small .p-button {
font-size: 0.875rem;
padding: 0.65625rem 1.09375rem;
}

View File

@@ -1,66 +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 { KeyValueRatio, User } from '../../../../../common-models/src/main.model';
import { SharedModule } from '../../shared/shared/shared.module';
import { SelectButtonModule } from 'primeng/selectbutton'; import { SelectButtonModule } from 'primeng/selectbutton';
export const stateOptions:KeyValueRatio[]=[ import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
{label:'16/9',value:16/9}, import { ImageService } from '../../services/image.service';
{label:'1/1',value:1}, import { LoadingService } from '../../services/loading.service';
{label:'2/3',value:2/3}, import { SharedModule } from '../../shared/shared/shared.module';
] 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);
} }
} }

View File

@@ -1,16 +1,17 @@
import { Component } from '@angular/core';
import { UserService } from '../../services/user.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
@Component({ @Component({
selector: 'logout', selector: 'logout',
standalone: true, standalone: true,
imports: [CommonModule,RouterModule], imports: [CommonModule, RouterModule],
template:`` template: ``,
}) })
export class LogoutComponent { export class LogoutComponent {
constructor(private userService:UserService){ constructor(public keycloakService: KeycloakService) {
userService.logout(); sessionStorage.removeItem('USERID');
keycloakService.logout(window.location.origin + '/home');
} }
} }

View File

@@ -0,0 +1 @@
<p>not-found works!</p>

View File

@@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found',
standalone: true,
template: '<h2>Page not found</h2>',
})
export class NotFoundComponent {}

View File

@@ -1,38 +1,42 @@
import { CanMatchFn, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; import { Injectable } from '@angular/core';
import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
// Services import { KeycloakInitializerService } from '../services/keycloak-initializer.service';
import { UserService } from '../services/user.service'; import { createLogger } from '../utils/utils';
const logger = createLogger('AuthGuard');
export const authGuard: CanMatchFn = async (route, segments): Promise<boolean | UrlTree> => { @Injectable({
const router = inject(Router); providedIn: 'root',
const userService = inject(UserService); })
export class AuthGuard extends KeycloakAuthGuard {
const authenticated: boolean = userService.isLoggedIn(); constructor(protected override readonly router: Router, protected readonly keycloak: KeycloakService, private keycloakInitializer: KeycloakInitializerService) {
if (!authenticated) { super(router, keycloak);
console.log(window.location.origin)
console.log(window.location.href)
await userService.login(`${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[] = userService.getUserRoles();//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.
const requiredRoles = route.data['roles'];
// Allow the user to proceed if no additional roles are required to access the route.
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) { if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
return true; return true;
} }
// Allow the user to proceed if ALL of the required roles are present // Allow the user to proceed if all the required roles are present.
const authorized = requiredRoles.every((role) => roles.includes(role)); return requiredRoles.every(role => this.roles.includes(role));
// Allow the user to proceed if ONE of the required roles is present
//const authorized = requiredRoles.some((role) => roles.includes(role));
if (authorized) {
return true;
} }
}
// Display my custom HTTP 403 access denied page
return router.createUrlTree(['/access']);
};

View File

@@ -0,0 +1,35 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class ListingCategoryGuard implements CanActivate {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
const id = route.paramMap.get('id');
const url = `${this.apiBaseUrl}/bizmatch/listings/undefined/${id}`;
return this.http.get<any>(url).pipe(
map(response => {
const category = response.listingsCategory;
if (category === 'business') {
return this.router.createUrlTree([`/details-business-listing/${id}`]);
} else if (category === 'commercialProperty') {
return this.router.createUrlTree([`/details-commercial-property-listing/${id}`]);
} else {
return this.router.createUrlTree(['/not-found']);
}
}),
catchError(() => {
return of(this.router.createUrlTree(['/not-found']));
}),
);
}
}

View File

@@ -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;
}

View File

@@ -1,24 +1,22 @@
import { HttpEvent, HttpHandler, HttpInterceptor, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable, inject } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs'; import { Observable, tap } from 'rxjs';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { LoadingService } from '../services/loading.service'; import { LoadingService } from '../services/loading.service';
@Injectable() @Injectable()
export class LoadingInterceptor implements HttpInterceptor { export class LoadingInterceptor implements HttpInterceptor {
constructor(private loadingService:LoadingService) { } constructor(private loadingService: LoadingService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
console.log("Intercepting Requests")
const requestId = `HTTP-${v4()}`; const requestId = `HTTP-${v4()}`;
this.loadingService.startLoading(requestId,request.url); this.loadingService.startLoading(requestId, request.url);
// return next.handle(request);
return next.handle(request).pipe( return next.handle(request).pipe(
tap({ tap({
finalize: () => this.loadingService.stopLoading(requestId), // Stoppt den Ladevorgang, wenn die Anfrage abgeschlossen ist finalize: () => this.loadingService.stopLoading(requestId), // Stoppt den Ladevorgang, wenn die Anfrage abgeschlossen ist
// Beachte, dass 'error' und 'complete' hier entfernt wurden, da 'finalize' in allen Fällen aufgerufen wird, // Beachte, dass 'error' und 'complete' hier entfernt wurden, da 'finalize' in allen Fällen aufgerufen wird,
// egal ob die Anfrage erfolgreich war, einen Fehler geworfen hat oder abgeschlossen wurde. // egal ob die Anfrage erfolgreich war, einen Fehler geworfen hat oder abgeschlossen wurde.
}) }),
); );
} }
} }

View File

@@ -1,52 +0,0 @@
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;
}

View File

@@ -1,142 +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;
}

View File

@@ -0,0 +1,122 @@
<div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{ listing?.title }}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
@if(historyService.canGoBack){
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="historyService.goBack()"></p-button>
}
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
@if(listing){
<div class="grid">
<div class="col-12 md:col-6">
<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">
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
<div class="text-900 w-full md:w-9 line-height-3 flex flex-column" [innerHTML]="description"></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">Category</div>
<div class="text-900 w-full md:w-9">
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
<div class="text-900 w-full md:w-9">{{ listing.city }}, {{ selectOptions.getState(listing.state) }}</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">Asking Price</div>
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Sales revenue</div>
<div class="text-900 w-full md:w-9">{{ listing.salesRevenue | currency }}</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">Cash flow</div>
<div class="text-900 w-full md:w-9">{{ listing.cashFlow | currency }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Type of Real Estate</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 class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-3 font-medium">Employees</div>
<div class="text-900 w-full md:w-9">{{ listing.employees }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Established since</div>
<div class="text-900 w-full md:w-9">{{ listing.established }}</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">Support & Training</div>
<div class="text-900 w-full md:w-9">{{ listing.supportAndTraining }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Reason for Sale</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>
</ul>
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/editBusinessListing', listing.id]"></button>
}
</div>
<div class="col-12 md:col-6">
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact the Author of this Listing</div>
<div class="font-italic text-sm text-900 mb-5">Please include your contact info below</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="name" class="font-medium text-900">Your Name</label>
<input id="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="email" class="font-medium text-900">Your Email</label>
<input id="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
<!-- <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 class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
<input id="state" type="text" pInputText [(ngModel)]="mailinfo.sender.state" />
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5" [(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
@if(listingUser){
<div class="surface-border mb-4 col-12 flex align-items-center">
Listing by &nbsp;<a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
@if(listingUser.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
}
</div>
}
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto" (click)="mail()"></button>
</div>
</div>
</div>
}
</div>
</div>
</div>

View File

@@ -0,0 +1,102 @@
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { InputMaskModule } from 'primeng/inputmask';
import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../../utils/utils';
@Component({
selector: 'app-details-business-listing',
standalone: true,
imports: [SharedModule, GalleriaModule, InputMaskModule],
providers: [MessageService],
templateUrl: './details-business-listing.component.html',
styleUrl: './details-business-listing.component.scss',
})
export class DetailsBusinessListingComponent {
// listings: Array<BusinessListing>;
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1,
},
];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: BusinessListing;
criteria: ListingCriteria;
mailinfo: MailInfo;
environment = environment;
keycloakUser: KeycloakUser;
user: User;
listingUser: User;
description: SafeHtml;
private history: string[] = [];
ts = new Date().getTime();
env = environment;
constructor(
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
private router: Router,
private userService: UserService,
public selectOptions: SelectOptionsService,
private mailService: MailService,
private messageService: MessageService,
private sanitizer: DomSanitizer,
public historyService: HistoryService,
public keycloakService: KeycloakService,
) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.history.push(event.urlAfterRedirects);
}
});
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
}
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.listingUser = await this.userService.getById(this.listing.userId);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
}
isAdmin() {
return this.keycloakService.getUserRoles(true).includes('ADMIN');
}
async mail() {
this.mailinfo.email = this.listingUser.email;
this.mailinfo.userId = this.listing.userId;
this.mailinfo.listing = this.listing;
await this.mailService.mail(this.mailinfo);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your message has been sent to the creator of the listing', life: 3000 });
}
}

View File

@@ -0,0 +1,101 @@
<div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center mb-2">
<div class="font-medium text-3xl text-900 mb-3">{{ listing?.title }}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
@if(historyService.canGoBack){
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="historyService.goBack()"></p-button>
}
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
@if(listing){
<div class="grid">
<div class="col-12 md:col-6">
<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">
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
<div class="text-900 w-full md:w-9 line-height-3" [innerHTML]="description"></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">Property Category</div>
<div class="text-900 w-full md:w-9">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
<div class="text-900 w-full md:w-9">{{ selectOptions.getState(listing.state) }}</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">City</div>
<div class="text-900 w-full md:w-9">{{ listing.city }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Zip Code</div>
<div class="text-900 w-full md:w-9">{{ listing.zipCode }}</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">County</div>
<div class="text-900 w-full md:w-9">{{ listing.county }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Asking Price:</div>
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
</li>
</ul>
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/editCommercialPropertyListing', listing.id]"></button>
}
</div>
<div class="col-12 md:col-6">
<p-galleria [value]="listing.imageOrder" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
<ng-template pTemplate="item" let-item>
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ item }}" style="width: 100%" />
</ng-template>
</p-galleria>
@if (mailinfo){
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact the Author of this Listing</div>
<div class="font-italic text-sm text-900 mb-5">Please include your contact info below</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="name" class="font-medium text-900">Your Name</label>
<input [ngClass]="{ 'ng-invalid': containsError('name'), 'ng-dirty': containsError('name') }" id="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="email" class="font-medium text-900">Your Email</label>
<input id="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
<!-- <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 class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
<input id="state" type="text" pInputText [(ngModel)]="mailinfo.sender.state" />
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5" [(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
@if(listingUser){
<div class="surface-border mb-4 col-12 flex align-items-center">
Listing by &nbsp;<a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
@if(listingUser.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imagePath }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
}
</div>
}
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto" (click)="mail()"></button>
</div>
}
</div>
</div>
}
</div>
</div>
</div>

View File

@@ -0,0 +1,107 @@
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { InputMaskModule } from 'primeng/inputmask';
import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../../utils/utils';
@Component({
selector: 'app-details-commercial-property-listing',
standalone: true,
imports: [SharedModule, GalleriaModule, InputMaskModule],
providers: [MessageService],
templateUrl: './details-commercial-property-listing.component.html',
styleUrl: './details-commercial-property-listing.component.scss',
})
export class DetailsCommercialPropertyListingComponent {
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1,
},
];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: CommercialPropertyListing;
criteria: ListingCriteria;
mailinfo: MailInfo;
environment = environment;
keycloakUser: KeycloakUser;
user: User;
listingUser: User;
description: SafeHtml;
ts = new Date().getTime();
env = environment;
errorResponse: ErrorResponse;
constructor(
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
private router: Router,
private userService: UserService,
public selectOptions: SelectOptionsService,
private mailService: MailService,
private messageService: MessageService,
private sanitizer: DomSanitizer,
public historyService: HistoryService,
public keycloakService: KeycloakService,
private imageService: ImageService,
) {
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
}
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, 'commercialProperty'));
this.listingUser = await this.userService.getById(this.listing.userId);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
}
isAdmin() {
return this.keycloakService.getUserRoles(true).includes('ADMIN');
}
async mail() {
this.mailinfo.email = this.listingUser.email;
this.mailinfo.userId = this.listing.userId;
this.mailinfo.listing = this.listing;
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 message has been sent to the creator of the listing', life: 3000 });
}
}
containsError(fieldname: string) {
return this.errorResponse?.fields.map(f => f.fieldname).includes(fieldname);
}
}

View File

@@ -1,141 +0,0 @@
<div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{listing?.title}}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
<div class="grid">
<div class="col-12 md:col-6">
<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">
<div class="text-500 w-full md:w-2 font-medium">Description</div>
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
</li>
@if (listing && (listing.listingsCategory==='business')){
<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-900 w-full md:w-10">
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
</div>
</li>
<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-900 w-full md:w-10">{{selectOptions.getState(listing.state)}}</div>
</li>
<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-900 w-full md:w-10">{{listing.price | currency}}</div>
</li>
<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-900 w-full md:w-10">{{listing.realEstateIncluded?'Yes':'No'}}</div>
</li>
<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-900 w-full md:w-10">{{listing.salesRevenue | currency}}</div>
</li>
<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-900 w-full md:w-10">{{listing.cashFlow | currency}}</div>
</li>
<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-900 w-full md:w-10">{{listing.employees}}</div>
</li>
<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-900 w-full md:w-10">{{listing.brokerLicencing}}</div>
</li>
}
@if (listing && (listing.listingsCategory==='commercialProperty')){
<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-900 w-full md:w-10">{{selectOptions.getCommercialProperty(listing.type)}}
</div>
</li>
<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-900 w-full md:w-10">{{selectOptions.getState(listing.state)}}</div>
</li>
<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-900 w-full md:w-10">{{listing.city}}</div>
</li>
<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-900 w-full md:w-10">{{listing.zipCode}}</div>
</li>
<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-900 w-full md:w-10">{{listing.county}}</div>
</li>
<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-900 w-full md:w-10">{{listing.price | currency}}</div>
</li>
}
</ul>
<p-galleria [value]="propertyImages" [showIndicators]="true" [showThumbnails]="false"
[responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }"
[numVisible]="5">
<ng-template pTemplate="item" let-item>
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{item.name}}"
style="width: 100%;" />
</ng-template>
<!-- <ng-template pTemplate="thumbnail" let-item>
<div class="grid grid-nogutter justify-content-center">
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{item.name}}" />
</div>
</ng-template> -->
</p-galleria>
@if(listing && user && (user.id===listing?.userId || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto"
[routerLink]="['/editListing',listing.id]"></button>
}
</div>
<div class="col-12 md:col-6">
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact The Author of This Listing
</div>
<div class="font-italic text-sm text-900 mb-5">Please Include your contact info below:</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="name" class="font-medium text-900">Your Name</label>
<input id="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name">
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="email" class="font-medium text-900">Your Email</label>
<input id="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email">
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
<input id="phoneNumber" type="text" pInputText
[(ngModel)]="mailinfo.sender.phoneNumber">
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
<input id="state" type="text" pInputText [(ngModel)]="mailinfo.sender.state">
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5"
[(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto"
(click)="mail()"></button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,95 +0,0 @@
import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { SelectOptionsService } from '../../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import data from '../../../../assets/data/listings.json';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { UserService } from '../../../services/user.service';
import onChange from 'on-change';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
import { ImageProperty, ListingCriteria, ListingType, MailInfo, User } from '../../../../../../common-models/src/main.model';
import { MailService } from '../../../services/mail.service';
import { MessageService } from 'primeng/api';
import { SharedModule } from '../../../shared/shared/shared.module';
import { GalleriaModule } from 'primeng/galleria';
import { environment } from '../../../../environments/environment';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-details-listing',
standalone: true,
imports: [SharedModule, GalleriaModule],
providers: [MessageService],
templateUrl: './details-listing.component.html',
styleUrl: './details-listing.component.scss'
})
export class DetailsListingComponent {
// listings: Array<BusinessListing>;
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1
}
];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
private type: 'business'|'commercialProperty' | undefined = this.activatedRoute.snapshot.params['type'] as 'business'|'commercialProperty' | undefined;
listing: ListingType;
criteria: ListingCriteria
mailinfo: MailInfo;
propertyImages: ImageProperty[] = []
environment = environment;
user:User
description:SafeHtml;
constructor(private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
private router: Router,
private userService: UserService,
public selectOptions: SelectOptionsService,
private mailService: MailService,
private messageService: MessageService,
private sanitizer: DomSanitizer) {
this.userService.getUserObservable().subscribe(user => {
this.user = user
});
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.mailinfo = { sender: {}, userId: '' }
}
async ngOnInit() {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, this.type));
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
this.description=this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
}
back() {
this.router.navigate(['listings', this.criteria.listingsCategory])
}
isAdmin() {
return this.userService.hasAdminRole();
}
async mail() {
this.mailinfo.userId = this.listing.userId;
await this.mailService.mail(this.mailinfo);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your message has been sent to the creator of the listing', life: 3000 });
}
}

View File

@@ -1,41 +1,36 @@
<div class="surface-ground h-full"> <div class="surface-ground h-full">
<div class="px-6 py-5"> <div class="px-6 py-5">
@if (user){
<div class="surface-card p-4 shadow-2 border-round"> <div class="surface-card p-4 shadow-2 border-round">
<!-- <div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{listing?.title}}</div>
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div> -->
<div class="surface-section px-6 pt-5"> <div class="surface-section px-6 pt-5">
<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="{{environment.apiBaseUrl}}/profile/{{user.id}}.avif" class="mr-5 mb-3 lg:mb-0" <img src="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
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" />
} }
<div> <div>
<span class="text-900 font-medium text-3xl">{{user.firstname}} {{user.lastname}}</span> <span class="text-900 font-medium text-3xl">{{ user.firstname }} {{ user.lastname }}</span>
<i class="pi pi-star text-2xl ml-4 text-yellow-500"></i> <i class="pi pi-star text-2xl ml-4 text-yellow-500"></i>
<div class="flex align-items-center flex-wrap text-sm"> <div class="flex align-items-center flex-wrap text-sm">
<div class="mr-5 mt-3"> <div class="mr-5 mt-3">
<span class="font-medium text-500">Company</span> <span class="font-medium text-500">Company</span>
<div class="text-700 mt-2">{{user.companyName}}</div> <div class="text-700 mt-2">{{ user.companyName }}</div>
</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">12</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>
<div class="text-700 mt-2">8</div> <div class="text-700 mt-2">8</div>
</div> </div> -->
<div class="flex align-items-center mt-3"> <div class="flex align-items-center mt-3">
<!-- <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="{{environment.apiBaseUrl}}/logo/{{user.id}}.avif" <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" />
class="mr-5 lg:mb-0" style="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" /> -->
@@ -44,11 +39,12 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@if(historyService.canGoBack){
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="historyService.goBack()"></p-button>
}
</div> </div>
<p class="mt-2 text-700 line-height-3 text-l font-semibold">{{user.description}}</p> <p class="mt-2 text-700 line-height-3 text-l font-semibold">{{ user.description }}</p>
</div> </div>
<div class="px-6 py-5"> <div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round"> <div class="surface-card p-4 shadow-2 border-round">
@@ -57,76 +53,95 @@
<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 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">Name</div> <div class="text-500 w-full md:w-2 font-medium">Name</div>
<div class="text-900 w-full md:w-10">{{user.firstname}} {{user.lastname}}</div> <div class="text-900 w-full md:w-10">{{ user.firstname }} {{ user.lastname }}</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">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>
<div class="text-900 w-full md:w-10 line-height-3">{{user.email}}</div> <div class="text-900 w-full md:w-10 line-height-3">{{ user.email }}</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">Company Location</div> <div class="text-500 w-full md:w-2 font-medium">Company Location</div>
<div class="text-900 w-full md:w-10 line-height-3">{{user.companyLocation}}</div> <div class="text-900 w-full md:w-10 line-height-3">{{ user.companyLocation }}</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">Services we offer</div> <div class="text-500 w-full md:w-2 font-medium">Services we offer</div>
<div class="text-900 w-full md:w-10" [innerHTML]="offeredServices"></div> <div class="text-900 w-full md:w-10" [innerHTML]="offeredServices"></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">Areas we serve</div> <div class="text-500 w-full md:w-2 font-medium">Areas (Counties) we serve</div>
<div class="text-900 w-full md:w-10"> <div class="text-900 w-full md:w-10">
@for (area of user.areasServed; track area) { @for (area of user.areasServed; track area) {
<p-tag styleClass="mr-2" [value]="area" [rounded]="true"></p-tag> <p-tag styleClass="mr-2" value="{{ area.county }}-{{ area.state }}" [rounded]="true"></p-tag>
} }
<!-- <p-tag styleClass="mr-2" severity="success" value="Javascript" [rounded]="true"></p-tag> <!-- <p-tag styleClass="mr-2" severity="success" value="Javascript" [rounded]="true"></p-tag>
<p-tag styleClass="mr-2" severity="danger" value="Python" [rounded]="true"></p-tag> <p-tag styleClass="mr-2" severity="danger" value="Python" [rounded]="true"></p-tag>
<p-tag severity="warning" value="SQL" [rounded]="true"></p-tag> --> <p-tag severity="warning" value="SQL" [rounded]="true"></p-tag> -->
</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">Licensed In</div> <div class="text-500 w-full md:w-2 font-medium">Licensed In</div>
<div class="text-900 w-full md:w-10"> <div class="text-900 w-full md:w-10">
@for (license of user.licensedIn; track license) { @for (license of user.licensedIn; track license) {
<div>{{license.name}} : {{license.value}}</div> <p-tag styleClass="mr-2" value="{{ license.registerNo }}-{{ license.state }}" [rounded]="true" severity="success"></p-tag>
} }
</div> </div>
</li> </li>
@if(businessListings?.length>0){
<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">My Listings For Sale</div> <div class="text-500 w-full md:w-2 font-medium">My Business Listings For Sale</div>
<div class="text-900 w-full md:w-10"> <div class="text-900 w-full md:w-10">
<div class="grid mt-0 mr-0"> <div class="grid mt-0 mr-0">
@for (listing of userListings; track listing) { @for (listing of businessListings; track listing) {
<div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-listing/business',listing.id]"> <div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-business-listing', listing.id]">
<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"> <div class="text-900 mb-2">
<span [class]="selectOptions.getBgColorType(listing.type)" <span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">
class="inline-flex border-circle align-items-center justify-content-center mr-3" <i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
style="width:38px;height:38px">
<i [class]="selectOptions.getIconAndTextColorType(listing.type)"
class="pi text-xl"></i>
</span> </span>
<span <span class="font-medium">{{ selectOptions.getBusiness(listing.type) }}</span>
class="font-medium">{{selectOptions.getBusiness(listing.type)}}</span>
</div> </div>
<div class="text-700">{{listing.title}}</div> <div class="text-700">{{ listing.title }}</div>
</div> </div>
</div> </div>
} }
</div> </div>
</div> </div>
</li> </li>
} @if(commercialPropListings?.length>0){
<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">My Commercial Property Listings For Sale</div>
<div class="text-900 w-full md:w-10">
<div class="grid mt-0 mr-0">
@for (listing of commercialPropListings; track listing) {
<div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-commercial-property-listing', listing.id]">
<div class="p-3 border-1 surface-border border-round surface-card">
<div class="text-900 mb-2 flex align-items-center">
@if (listing.imageOrder?.length>0){
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
} @else {
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
}
<span class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</span>
</div>
<div class="text-700">{{ listing.title }}</div>
</div>
</div>
}
</div>
</div>
</li>
}
</ul> </ul>
</div> </div>
</div> </div>
@if( user?.id===(user$| async)?.id || isAdmin()){ @if( user?.email===keycloakUser?.email || isAdmin()){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" <button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/account', user.id]"></button>
[routerLink]="['/account',user.id]"></button>
} }
</div> </div>
}
</div> </div>
</div> </div>

View File

@@ -1,16 +1,20 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SharedModule } from '../../../shared/shared/shared.module'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { GalleriaModule } from 'primeng/galleria';
import { MessageService } from 'primeng/api';
import { BusinessListing, ListingCriteria, ListingType, User } from '../../../../../../common-models/src/main.model';
import { environment } from '../../../../environments/environment';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { UserService } from '../../../services/user.service'; import { KeycloakService } from 'keycloak-angular';
import { MessageService } from 'primeng/api';
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 { KeycloakUser, ListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.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 { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { UserService } from '../../../services/user.service';
import { ImageService } from '../../../services/image.service'; import { SharedModule } from '../../../shared/shared/shared.module';
import { formatPhoneNumber, map2User } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-details-user', selector: 'app-details-user',
@@ -18,39 +22,50 @@ import { ImageService } from '../../../services/image.service';
imports: [SharedModule, GalleriaModule], imports: [SharedModule, GalleriaModule],
providers: [MessageService], providers: [MessageService],
templateUrl: './details-user.component.html', templateUrl: './details-user.component.html',
styleUrl: './details-user.component.scss' styleUrl: './details-user.component.scss',
}) })
export class DetailsUserComponent { export class DetailsUserComponent {
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;
user$:Observable<User> user$: Observable<KeycloakUser>;
keycloakUser: KeycloakUser;
environment = environment; environment = environment;
criteria:ListingCriteria; criteria: ListingCriteria;
userListings:BusinessListing[] businessListings: BusinessListing[];
companyOverview:SafeHtml; commercialPropListings: CommercialPropertyListing[];
offeredServices:SafeHtml; companyOverview: SafeHtml;
constructor(private activatedRoute: ActivatedRoute, offeredServices: SafeHtml;
ts = new Date().getTime();
env = environment;
emailToDirName = emailToDirName;
formatPhoneNumber = formatPhoneNumber;
constructor(
private activatedRoute: ActivatedRoute,
private router: Router, private router: Router,
private userService: UserService, private userService: UserService,
private listingsService:ListingsService, private listingsService: ListingsService,
private messageService: MessageService, private messageService: MessageService,
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private imageService:ImageService) { private imageService: ImageService,
} public historyService: HistoryService,
public keycloakService: KeycloakService,
) {}
async ngOnInit() { async ngOnInit() {
this.user = await this.userService.getById(this.id); this.user = await this.userService.getById(this.id);
const results = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
this.businessListings = results[0];
this.commercialPropListings = results[1];
//this.user$ = this.userService.getUserObservable();
const token = await this.keycloakService.getToken();
this.keycloakUser = map2User(token);
this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview);
this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices);
}
this.userListings = await this.listingsService.getListingByUserId(this.id);
this.user$ = this.userService.getUserObservable();
this.companyOverview=this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview);
this.offeredServices=this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices);
}
back() {
this.router.navigate(['listings', this.criteria.listingsCategory])
}
isAdmin() { isAdmin() {
return this.userService.hasAdminRole(); return this.keycloakService.getUserRoles(true).includes('ADMIN');
} }
} }

View File

@@ -1,16 +1,15 @@
<div class="container"> <div class="container">
<div class="wrapper"> <div class="wrapper">
<div class="py-3 px-6 flex align-items-center justify-content-between relative"> <div class="py-3 px-6 flex align-items-center justify-content-between relative">
<a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" ></a> <a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" /></a>
<div <div class="align-items-center flex-grow-1 justify-content-between hidden lg:flex absolute lg:static w-full left-0 top-100 px-6 lg:px-0 shadow-2 lg:shadow-none z-2">
class="align-items-center flex-grow-1 justify-content-between hidden lg:flex absolute lg:static w-full left-0 top-100 px-6 lg:px-0 shadow-2 lg:shadow-none z-2">
<section></section> <section></section>
<div <div class="flex justify-content-between lg:block border-top-1 lg:border-top-none border-gray-800 py-3 lg:py-0 mt-3 lg:mt-0">
class="flex justify-content-between lg:block border-top-1 lg:border-top-none border-gray-800 py-3 lg:py-0 mt-3 lg:mt-0"> @if(keycloakService.isLoggedIn()){
@if(userService.isLoggedIn()){ <p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" [routerLink]="['/account']"></p-button>
<p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" [routerLink]="['/account',(user$|async)?.id]"></p-button>
} @else { } @else {
<p-button label="Log In" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="login()"></p-button> <p-button label="Log In" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="login()"></p-button>
<p-button label="Register" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="register()"></p-button>
} }
</div> </div>
</div> </div>
@@ -19,55 +18,84 @@
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-12 lg:w-6 p-4"> <div class="w-12 lg:w-6 p-4">
<h1 class="text-6xl font-bold text-blue-900 mt-0 mb-3">Find businesses for sale</h1> <h1 class="text-6xl font-bold text-blue-900 mt-0 mb-3">Find businesses for sale</h1>
<p class="text-3xl text-blue-600 mt-0 mb-5">Arcu cursus euismod quis viverra nibh cras. Amet justo <p class="text-3xl text-blue-600 mt-0 mb-5">Unlocking Exclusive Opportunities, Empowering Entrepreneurial Dreams</p>
donec
enim diam vulputate ut.</p>
<ul class="list-none p-0 m-0"> <ul class="list-none p-0 m-0">
<li class="mb-3 flex align-items-center"><i <li class="mb-3 flex align-items-center"><i class="pi pi-compass text-yellow-500 text-xl mr-2"></i><span class="text-blue-600 line-height-3">Texas expertise and nationwide presence</span></li>
class="pi pi-compass text-yellow-500 text-xl mr-2"></i><span <li class="mb-3 flex align-items-center"><i class="pi pi-map text-yellow-500 text-xl mr-2"></i><span class="text-blue-600 line-height-3">Industry diversity</span></li>
class="text-blue-600 line-height-3">Senectus et netus et malesuada fames.</span></li> <li class="mb-3 flex align-items-center"><i class="pi pi-calendar text-yellow-500 text-xl mr-2"></i><span class="text-blue-600 line-height-3">Support throughout the entire process</span></li>
<li class="mb-3 flex align-items-center"><i
class="pi pi-map text-yellow-500 text-xl mr-2"></i><span
class="text-blue-600 line-height-3">Orci a scelerisque purus semper eget.</span></li>
<li class="mb-3 flex align-items-center"><i
class="pi pi-calendar text-yellow-500 text-xl mr-2"></i><span
class="text-blue-600 line-height-3">Aenean sed adipiscing diam donec adipiscing
tristique.</span></li>
</ul> </ul>
</div> </div>
<div class="w-12 lg:w-6 text-center lg:text-right flex"> <div class="w-12 lg:w-6 text-center lg:text-right flex">
<div class="mt-5"> <div class="mt-5">
<ul class="flex flex-column align-items-left gap-3 px-2 py-3 list-none surface-border"> <ul class="flex flex-column align-items-left gap-3 px-2 py-3 list-none surface-border">
<li><button pButton pRipple icon="pi pi-user" (click)="activeTabAction = 'business'" <li><button pButton pRipple icon="pi pi-user" (click)="changeTab('business')" label="Businesses" [ngClass]="{ 'p-button-text text-700': activeTabAction !== 'business' }"></button></li>
label="Businesses" <li>
[ngClass]="{'p-button-text text-700': activeTabAction !== 'business'}"></button></li> <button
<li><button pButton pRipple icon="pi pi-globe" (click)="activeTabAction = 'professionals_brokers'" pButton
label="Professionals/Brokers Directory" pRipple
[ngClass]="{'p-button-text text-700': activeTabAction != 'professionals_brokers'}"></button></li> icon="pi pi-shield"
<li><button pButton pRipple icon="pi pi-shield" (click)="activeTabAction = 'commercialProperty'" (click)="changeTab('commercialProperty')"
label="Commercial Property" label="Commercial Property"
[ngClass]="{'p-button-text text-700': activeTabAction != 'commercialProperty'}"></button> [ngClass]="{ 'p-button-text text-700': activeTabAction != 'commercialProperty' }"
></button>
</li>
<li>
<button pButton pRipple icon="pi pi-globe" (click)="changeTab('broker')" label="Professionals/Brokers Directory" [ngClass]="{ 'p-button-text text-700': activeTabAction != 'broker' }"></button>
</li> </li>
</ul> </ul>
</div> </div>
<div class="mt-5"> <div [ngClass]="{ 'mt-5': activeTabAction === 'business', 'mt-11': activeTabAction === 'commercialProperty', 'mt-22': activeTabAction === 'broker' }">
<div class="flex flex-column align-items-right gap-3 px-2 py-3 my-3 surface-border"> <div class="flex flex-column align-items-right gap-3 px-2 py-3 my-3 surface-border">
<p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name" <p-dropdown
optionValue="value" [showClear]="true" placeholder="Category" [filter]="true"
[style]="{ width: '200px'}"></p-dropdown> filterBy="name"
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name" optionValue="value" [options]="states"
[showClear]="true" placeholder="Min Price" [style]="{ width: '200px'}"></p-dropdown> [(ngModel)]="criteria.state"
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name" optionValue="value" optionLabel="name"
[showClear]="true" placeholder="Max Price" [style]="{ width: '200px'}"></p-dropdown> optionValue="value"
<button pButton pRipple label="Find" class="ml-3 font-bold" [showClear]="true"
[style]="{ width: '170px'}" (click)="search()"></button> placeholder="State"
[style]="{ width: '200px' }"
></p-dropdown>
@if(activeTabAction === 'business'){
<p-dropdown
[filter]="true"
filterBy="name"
[options]="selectOptions.typesOfBusiness"
[(ngModel)]="criteria.type"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="Category"
[style]="{ width: '200px' }"
></p-dropdown>
} @if(activeTabAction === 'commercialProperty'){
<p-dropdown
[options]="selectOptions.typesOfCommercialProperty"
[(ngModel)]="criteria.type"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="Category"
[style]="{ width: '200px' }"
></p-dropdown>
} @if(activeTabAction === 'business' || activeTabAction === 'commercialProperty'){
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Min Price" [style]="{ width: '200px' }"></p-dropdown>
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Max Price" [style]="{ width: '200px' }"></p-dropdown>
}
<button pButton pRipple label="Find" class="ml-3 font-bold" [style]="{ width: '200px' }" (click)="search()"></button>
</div> </div>
</div> </div>
</div> </div>
<div class="w-12 flex justify-content-center"> <div class="w-12 flex justify-content-center">
<button type="button" pButton pRipple label="Create Your Listing" <button
class="block mt-7 mb-7 lg:mb-0 p-button-rounded p-button-success p-button-lg font-medium" [routerLink]="userService.isLoggedIn()?'/createListing':'/pricing'"></button> type="button"
pButton
pRipple
label="Create Your Listing"
class="block mt-7 mb-7 lg:mb-0 p-button-rounded p-button-success p-button-lg font-medium"
[routerLink]="keycloakService.isLoggedIn() ? '/createBusinessListing' : '/pricing'"
></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,17 +1,24 @@
:host { :host {
height: 100% height: 100%;
} }
.container { .container {
background-image: url(../../../assets/images/index-bg.webp); background-image: url(../../../assets/images/index-bg.webp);
//background-image: url(../../../assets/images/corpusChristiSkyline.jpg); // background-image: url(../../../assets/images/1_Version.jpg);
//background-image: url(../../../assets/images/2_1_Version.jpg);
background-size: cover; background-size: cover;
background-position: center; background-position: center;
height: 100vh; height: 100vh;
} }
.combo_lp{ .combo_lp {
width: 200px; width: 200px;
} }
.p-button-white{ .p-button-white {
color:aliceblue color: aliceblue;
}
.mt-11 {
margin-top: 5.9rem !important;
}
.mt-22 {
margin-top: 9.7rem !important;
} }

Some files were not shown because too many files have changed in this diff Show More