77 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
e784b424b0 200 datasets 2024-04-08 17:15:22 +02:00
4a9798c85e redis-om 2024-04-08 17:11:14 +02:00
5c6dfa1e0d output 2024-04-08 17:00:06 +02:00
8aea819496 data & perftest 2024-04-08 16:56:30 +02:00
fb69d2e4b3 favicon & json data 2024-04-07 19:46:58 +02:00
e5b7779492 removed unneeded files 2024-04-07 10:34:28 +02:00
a437851f6d drag & drop renewed, imageCropper revisited, imageOrder persisted, css quirks 2024-03-31 19:44:08 +02:00
89bb85a512 image-cropper component, drag & drop bilder 2024-03-29 20:59:34 +01:00
840d7a63b1 broker direcrtory renewed, imageservice updated, demo data 2024-03-25 20:17:49 +01:00
73ab12a694 Aufteilung details in user & listings, listings by user 2024-03-24 20:42:59 +01:00
a2c613c38f cropper & imageservice 2024-03-23 22:35:47 +01:00
d5210d3df4 areasServed ist string[] 2024-03-18 18:18:13 +01:00
fd91adda57 Image Upload, spinner aktivirt, listings & details überarbeitet 2024-03-18 18:17:04 +01:00
2b27ab8ba5 Ansichten verbessert - 1. Teil 2024-03-17 20:29:53 +01:00
25b201a9c6 Neue Email Adresse 2024-03-16 11:48:10 +01:00
b8cabf4f0c statische Webpage 2024-03-15 15:40:28 +01:00
f3868da8f8 Auth korrigiert & mail based on SES 2024-03-12 20:26:58 +01:00
be146fdc6a fix inputnumber, Umbau auf redis-om, Neubenamung 2024-03-11 18:27:43 +01:00
6ad40b6dca geo coding, user service, listing for business 2024-03-10 20:59:23 +01:00
06f349d1c3 Umstellung auf ACL 2024-03-01 15:35:51 -06:00
c7fccd46d9 changed path 2024-03-01 14:57:03 -06:00
3b0196bd66 change to tls & remote 2024-03-01 14:45:25 -06:00
244fbe78ed test change 2024-03-01 13:19:04 -06:00
56648b47a2 realm changed for localhost 2024-02-29 17:53:51 -06:00
f3b29a362e add SEO & Term of use & privacy 2024-02-29 13:19:10 -06:00
fa3b9e104a removed 2024-02-29 12:27:44 -06:00
798b84aa88 remove Professionals from SelectList 2024-02-29 12:21:56 -06:00
Andreas Knuth
0e01b6ba09 changed path 2024-02-29 12:08:39 -06:00
Andreas Knuth
3f9ffc1616 deleted 2024-02-29 12:06:16 -06:00
29a2eacd0f removed docker folder 2024-02-29 11:57:21 -06:00
2eb34adc7c auth controller 2024-02-29 11:24:23 -06:00
293 changed files with 187855 additions and 5663 deletions

6
.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,10 +65,7 @@ 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
*.jar *.jar
gitea
auth

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

@@ -55,4 +55,5 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
public 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

@@ -1,7 +0,0 @@
/// <reference types="multer" />
import { FileService } from '../file/file.service.js';
export declare class AccountController {
private fileService;
constructor(fileService: FileService);
uploadFile(file: Express.Multer.File, id: string): void;
}

View File

@@ -1,2 +0,0 @@
export declare class AccountService {
}

View File

@@ -1,6 +0,0 @@
import { AppService } from './app.service.js';
export declare class AppController {
private readonly appService;
constructor(appService: AppService);
getHello(): string;
}

View File

@@ -1,2 +0,0 @@
export declare class AppModule {
}

View File

@@ -1,3 +0,0 @@
export declare class AppService {
getHello(): string;
}

View File

@@ -1,66 +0,0 @@
[
{
"id":"1",
"userId":"1",
"listingsCategory": "business",
"title": "Industrial Service Company In Corpus Christi For Sale - 1954",
"summary": ["Asking price: $5,500,000","Sales revenue: $1,200,000","Net profit: $650,000"],
"description": ["This company services a wide variety of industries. Asking price includes Business and the Real Estate and is approx 30,000 sq ft with room for expansion including approx 5 acres. Absentee run business."],
"type": "2",
"location": "Texas",
"price":5500000,
"salesRevenue":1200000,
"cashFlow":650000,
"brokerLicencing":"TREC Broker #516788",
"established":1954,
"realEstateIncluded":true
},
{
"id":"2",
"userId":"1",
"listingsCategory": "business",
"title": "Coastal Bend Manufacturing Business Plastic Injection For Sale - 1950",
"summary": ["Asking price: $165,000","Sales revenue: Undisclosed","Net profit: Undisclosed"],
"description": [""],
"type": "12",
"location": "Texas",
"price":165000,
"salesRevenue":null,
"cashFlow":null,
"brokerLicencing":"TREC Broker #516788",
"established":1950,
"realEstateIncluded":false
},
{
"id":"3",
"userId":"1",
"listingsCategory": "business",
"title": "Corner Property On Everhart South-side Corpus Christi For Sale - 1944",
"summary": ["Asking price: $830,000","Sales revenue: Undisclosed","Net profit: Undisclosed"],
"description": [""],
"type": "3",
"location": "Texas",
"price":830000,
"salesRevenue":null,
"cashFlow":null,
"brokerLicencing":"TREC Broker #516788",
"established":1944,
"realEstateIncluded":false
},
{
"id":"4",
"userId":"1",
"listingsCategory": "business",
"title": "Corpus Christi Dessert Business For Sale - 1941",
"summary": ["Asking price: $124,900","Sales revenue: $225,000","Net profit: $50,000"],
"description": [""],
"type": "13",
"location": "Texas",
"price":830000,
"salesRevenue":225000,
"cashFlow":50000,
"brokerLicencing":"TREC Broker #516788",
"established":1941,
"realEstateIncluded":false
}
]

View File

@@ -1,14 +0,0 @@
[{
"id":"1",
"userId":"e0811669-c7eb-4e5e-a699-e8334d5c5b01",
"level":"Business Broker",
"start":"2024-02-12T21:54:20.603Z",
"modified":"2024-02-12T21:54:20.603Z",
"end":"9999-02-12T21:54:20.603Z",
"status":"active",
"invoices":[{
"date":"2024-02-12T21:54:20.603Z",
"id":"C991853B99",
"price":0
}]
}]

View File

@@ -1,8 +0,0 @@
/// <reference types="multer" />
export declare class FileService {
private subscriptions;
constructor();
private loadSubscriptions;
getSubscriptions(): any;
storeFile(file: Express.Multer.File, id: string): Promise<void>;
}

View File

@@ -1,13 +0,0 @@
import { ListingsService } from './listings.service.js';
import { Logger } from 'winston';
export declare class ListingsController {
private readonly listingsService;
private readonly logger;
constructor(listingsService: ListingsService, logger: Logger);
findAll(): any;
findById(id: string): any;
find(criteria: any): any;
updateById(id: string, listing: any): void;
create(listing: any): void;
deleteById(id: string): void;
}

View File

@@ -1,13 +0,0 @@
import { BusinessListing, InvestmentsListing, ListingCriteria, ProfessionalsBrokersListing } from '../models/main.model.js';
import { RedisService } from '../redis/redis.service.js';
import { Logger } from 'winston';
export declare class ListingsService {
private redisService;
private readonly logger;
constructor(redisService: RedisService, logger: Logger);
setListing(value: BusinessListing | ProfessionalsBrokersListing | InvestmentsListing, id?: string): Promise<void>;
getListingById(id: string): Promise<any>;
deleteListing(id: string): void;
getAllListings(start?: number, end?: number): Promise<any>;
find(criteria: ListingCriteria): Promise<any>;
}

View File

@@ -1,7 +0,0 @@
import { MailService } from './mail.service.js';
import { MailInfo } from '../models/server.model.js';
export declare class MailController {
private mailService;
constructor(mailService: MailService);
sendEMail(id: string, mailInfo: MailInfo): any;
}

View File

@@ -1,2 +0,0 @@
export declare class MailModule {
}

View File

@@ -1,9 +0,0 @@
import { MailerService } from '@nestjs-modules/mailer';
import { AuthService } from '../auth/auth.service.js';
import { MailInfo } from '../models/server.model.js';
export declare class MailService {
private mailerService;
private authService;
constructor(mailerService: MailerService, authService: AuthService);
sendInquiry(userId: string, mailInfo: MailInfo): Promise<void>;
}

View File

@@ -1,5 +0,0 @@
<p>Hey {{ name }},</p>
<p>You got an inquiry a</p>
<p>
{{inquiry}}
</p>

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1,126 +0,0 @@
export interface KeyValue {
name: string;
value: string;
}
export interface KeyValueStyle {
name: string;
value: string;
icon: string;
bgColorClass: string;
textColorClass: string;
}
export type SelectOption<T = number> = {
value: T;
label: string;
};
export interface Listing {
id: string;
userId: string;
title: string;
description: Array<string>;
location: string;
favoritesForUser: Array<string>;
hideImage?: boolean;
created: Date;
updated: Date;
}
export interface BusinessListing extends Listing {
listingsCategory: 'business';
summary: Array<string>;
type: string;
price?: number;
realEstateIncluded?: boolean;
salesRevenue?: number;
cashFlow?: number;
netProfit?: number;
inventory?: string;
employees?: number;
established?: number;
reasonForSale?: string;
brokerLicencing?: string;
internals?: string;
}
export interface ProfessionalsBrokersListing extends Listing {
listingsCategory: 'professionals_brokers';
summary: string;
address?: string;
email?: string;
website?: string;
category?: 'Professionals' | 'Broker';
}
export interface InvestmentsListing extends Listing {
listingsCategory: 'investment';
email?: string;
website?: string;
phoneNumber?: string;
}
export type ListingType = BusinessListing | ProfessionalsBrokersListing | InvestmentsListing;
export interface ListingCriteria {
type: string;
location: string;
minPrice: string;
maxPrice: string;
realEstateChecked: boolean;
listingsCategory: 'business' | 'professionals_brokers' | 'investment';
category: 'professional|broker';
}
export interface User {
id: string;
username: string;
firstname: string;
lastname: string;
email: string;
}
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;
}
interface Resourceaccess {
account: Realmaccess;
}
interface Realmaccess {
roles: string[];
}
export interface PageEvent {
first: number;
rows: number;
page: number;
pageCount: number;
}
export {};

View File

@@ -1,129 +0,0 @@
export interface KeyValue {
name: string;
value: string;
}
export interface KeyValueStyle {
name: string;
value: string;
icon:string;
bgColorClass:string;
textColorClass:string;
}
export type SelectOption<T = number> = {
value: T;
label: string;
};
export interface Listing {
id: string;
userId: string;
title: string;
description: Array<string>;
location: string;//enum
favoritesForUser:Array<string>;
hideImage?:boolean;
created:Date;
updated:Date;
}
export interface BusinessListing extends Listing {
listingsCategory: 'business'; //enum
summary: Array<string>;
type: string; //enum
price?: number;
realEstateIncluded?: boolean;
salesRevenue?: number;
cashFlow?: number;
netProfit?: number;
inventory?: string;
employees?: number;
established?: number;
reasonForSale?: string;
brokerLicencing?: string;
internals?: string;
}
export interface ProfessionalsBrokersListing extends Listing {
listingsCategory: 'professionals_brokers'; //enum
summary: string;
address?: string;
email?: string;
website?: string;
category?: 'Professionals' | 'Broker';
}
export interface InvestmentsListing extends Listing {
listingsCategory: 'investment'; //enum
email?: string;
website?: string;
phoneNumber?: string;
}
export type ListingType =
| BusinessListing
| ProfessionalsBrokersListing
| InvestmentsListing;
export interface ListingCriteria {
type:string,
location:string,
minPrice:string,
maxPrice:string,
realEstateChecked:boolean,
listingsCategory:'business'|'professionals_brokers'|'investment',
category:'professional|broker'
}
export interface User {
id: string;
username: string;
firstname: string;
lastname: string;
email: string;
}
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;
}
interface Resourceaccess {
account: Realmaccess;
}
interface Realmaccess {
roles: string[];
}
export interface PageEvent {
first: number;
rows: number;
page: number;
pageCount: number;
}

View File

@@ -1,11 +0,0 @@
export interface MailInfo {
sender: Sender;
userId: string;
}
export interface Sender {
name: string;
email: string;
phoneNumber: string;
state: string;
comments: string;
}

View File

@@ -1,5 +0,0 @@
import { RedisService } from './redis.service.js';
export declare class RedisController {
private redisService;
constructor(redisService: RedisService);
}

View File

@@ -1,2 +0,0 @@
export declare class RedisModule {
}

View File

@@ -1,11 +0,0 @@
export declare const LISTINGS = "LISTINGS";
export declare const SUBSCRIPTIONS = "SUBSCRIPTIONS";
export declare const USERS = "USERS";
export declare class RedisService {
private redis;
setJson(id: string, value: any): Promise<void>;
delete(id: string): Promise<void>;
getJson(id: string, prefix: string): Promise<any>;
getId(prefix: 'LISTINGS' | 'SUBSCRIPTIONS' | 'USERS'): Promise<string>;
search(prefix: 'LISTINGS' | 'SUBSCRIPTIONS' | 'USERS', clause: string): Promise<any>;
}

View File

@@ -1,6 +0,0 @@
import { SelectOptionsService } from './select-options.service.js';
export declare class SelectOptionsController {
private selectOptionsService;
constructor(selectOptionsService: SelectOptionsService);
getSelectOption(): any;
}

View File

@@ -1,10 +0,0 @@
import { KeyValue, KeyValueStyle } from '../models/main.model.js';
export declare class SelectOptionsService {
constructor();
typesOfBusiness: Array<KeyValueStyle>;
prices: Array<KeyValue>;
listingCategories: Array<KeyValue>;
categories: Array<KeyValueStyle>;
private usStates;
locations: Array<any>;
}

View File

@@ -1,6 +0,0 @@
import { FileService } from '../file/file.service.js';
export declare class SubscriptionsController {
private readonly fileService;
constructor(fileService: FileService);
findAll(): any;
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
export declare function convertStringToNullUndefined(value: any): any;

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,8 +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",
"ioredis": "^5.3.2", "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",
@@ -39,12 +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-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",
"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",
@@ -56,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,14 +0,0 @@
import { Controller, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileService } from '../file/file.service.js';
@Controller('account')
export class AccountController {
constructor(private fileService:FileService){}
@Post('uploadPhoto/:id')
@UseInterceptors(FileInterceptor('file'),)
uploadFile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
this.fileService.storeFile(file,id);
}
}

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,33 +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 { ListingsController } from './listings/listings.controller.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 { AccountController } from './account/account.controller.js';
import { AccountService } from './account/account.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 { 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(), RedisModule, MailModule, AuthModule, imports: [
ServeStaticModule.forRoot({ ConfigModule.forRoot({ isGlobal: true }),
rootPath: join(__dirname, '..', 'public'), // `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({
@@ -43,9 +68,19 @@ const __dirname = path.dirname(__filename);
// other transports... // other transports...
], ],
// other options // other options
}) }),
GeoModule,
UserModule,
ListingsModule,
SelectOptionsModule,
ImageModule,
PassportModule,
], ],
controllers: [AppController, ListingsController, SelectOptionsController, SubscriptionsController, AccountController], controllers: [AppController],
providers: [AppService, FileService, SelectOptionsService, ListingsService, AccountService], providers: [AppService, FileService],
}) })
export class AppModule {} export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestDurationMiddleware).forRoutes('*');
}
}

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,40 @@
import { Controller, Get, Param, Put } from '@nestjs/common';
import { AuthService } from './auth.service.js';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get()
getAccessToken(): any {
return this.authService.getAccessToken();
}
@Get('users')
getUsers(): any {
return this.authService.getUsers();
}
@Get('user/:userid')
getUser(@Param('userid') userId: string): any {
return this.authService.getUser(userId);
}
@Get('groups')
getGroups(): any {
return this.authService.getGroups();
}
@Get('user/:userid/groups') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
getGroupsForUsers(@Param('userid') userId: string): any {
return this.authService.getGroupsForUser(userId);
}
@Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
getLastLogin(@Param('userid') userId: string): any {
return this.authService.getLastLogin(userId);
}
@Put('user/:userid/group/:groupid') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth //
addUser2Group(@Param('userid') userId: string,@Param('groupid') groupId: string): any {
return this.authService.addUser2Group(userId,groupId);
}
}

View File

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import path from 'path';
import { fileURLToPath } from 'url';
import { JwtStrategy } from '../jwt.strategy.js';
import { AuthController } from './auth.controller.js';
import { AuthService } from './auth.service.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@Module({
imports: [PassportModule],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,107 @@
import { Injectable } from '@nestjs/common';
// import got from 'got';
import ky from 'ky';
import urlcat from 'urlcat';
@Injectable()
export class AuthService {
public async getAccessToken() {
const form = new FormData();
form.append('grant_type', 'password');
form.append('username', process.env.user);
form.append('password', process.env.password);
try {
const params = new URLSearchParams();
params.append('grant_type', 'password');
params.append('username', process.env.user);
params.append('password', process.env.password);
const URL = `${process.env.host}${process.env.tokenURL}`;
const response = await ky.post(URL, {
body: params.toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3'
},
}).json();
return (<any>response).access_token;
} catch (error) {
if (error.name === 'HTTPError') {
const errorJson = await error.response.json();
console.error('Fehlerantwort vom Server:', errorJson);
} else {
console.error('Allgemeiner Fehler:', error);
}
}
}
public async getUsers(){
const token = await this.getAccessToken();
const URL = `${process.env.host}${process.env.usersURL}`;
const response = await ky.get(URL, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':`Bearer ${token}`
},
}).json();
return response
}
public async getUser(userid:string){
const token = await this.getAccessToken();
const URL = urlcat(process.env.host,process.env.userURL,{userid})
const response = await ky.get(URL, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':`Bearer ${token}`
},
}).json();
return response
}
public async getGroups(){
const token = await this.getAccessToken();
const URL = `${process.env.host}${process.env.groupsURL}`;
const response = await ky.get(URL, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':`Bearer ${token}`
},
}).json();
return response
}
public async getGroupsForUser(userid:string){
const token = await this.getAccessToken();
const URL = urlcat(process.env.host,process.env.userGroupsURL,{userid})
const response = await ky.get(URL, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':`Bearer ${token}`
},
}).json();
return response
}
public async getLastLogin(userid:string){
const token = await this.getAccessToken();
const URL = urlcat(process.env.host,process.env.lastLoginURL,{userid})
const response = await ky.get(URL, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':`Bearer ${token}`
},
}).json();
return response
}
public async addUser2Group(userid:string,groupid:string){
const token = await this.getAccessToken();
const URL = urlcat(process.env.host,process.env.addUser2GroupURL,{userid,groupid})
const response = await ky.put(URL, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization':`Bearer ${token}`
},
}).json();
return response
}
}

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,29 +1,149 @@
import { 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 { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import path, { join } from 'path';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
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);
@Injectable() @Injectable()
export class FileService { export class FileService {
private subscriptions: any; private subscriptions: any;
constructor() { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
this.loadSubscriptions(); this.loadSubscriptions();
fs.ensureDirSync(`./pictures`);
fs.ensureDirSync(`./pictures/profile`);
fs.ensureDirSync(`./pictures/logo`);
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;
}
// ############
// Profile
// ############
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
let quality = 50;
const output = await sharp(file.buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
}
hasProfile(adjustedEmail: string) {
return fs.existsSync(`./pictures/profile/${adjustedEmail}.avif`);
}
// ############
// Logo
// ############
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
let quality = 50;
const output = await sharp(file.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);
}
hasCompanyLogo(adjustedEmail: string) {
return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
}
// ############
// Property
// ############
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
const result: string[] = [];
const directory = `./pictures/property/${imagePath}/${serial}`;
if (fs.existsSync(directory)) {
const files = await fs.readdir(directory);
files.forEach(f => {
result.push(f);
});
return result;
} else {
return [];
}
}
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
const result: ImageProperty[] = [];
const directory = `./pictures/property/${imagePath}/${serial}`;
if (fs.existsSync(directory)) {
const files = await fs.readdir(directory);
return files.length > 0;
} else {
return false;
}
}
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
const directory = `./pictures/property/${imagePath}/${serial}`;
fs.ensureDirSync(`${directory}`);
const imageName = await this.getNextImageName(directory);
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
return `${imageName}.avif`;
}
// ############
// utils
// ############
async getNextImageName(directory) {
try {
const files = await fs.readdir(directory);
const imageNumbers = files
.map(file => parseInt(file, 10)) // Dateinamen direkt in Zahlen umwandeln
.filter(number => !isNaN(number)) // Sicherstellen, dass die Konvertierung gültig ist
.sort((a, b) => a - b); // Aufsteigend sortieren
const nextNumber = imageNumbers.length > 0 ? Math.max(...imageNumbers) + 1 : 1;
return `${nextNumber}`; // Keine Endung für den Dateinamen
} catch (error) {
console.error('Fehler beim Lesen des Verzeichnisses:', error);
return null;
}
}
async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) {
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
let output;
let start = Date.now();
output = await sharp(buffer)
.resize({ width: 1500 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
let timeTaken = Date.now() - start;
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
}
deleteImage(path: string) {
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);
} }
async storeFile(file: Express.Multer.File,id: string){
const suffix = file.mimetype.includes('png')?'png':'jpg'
await fs.outputFile(`./public/profile_${id}`,file.buffer);
} }
} }

View File

@@ -0,0 +1,18 @@
import { Controller, Get, Param } from '@nestjs/common';
import { GeoService } from './geo.service.js';
@Controller('geo')
export class GeoController {
constructor(private geoService:GeoService){}
@Get(':prefix')
findByPrefix(@Param('prefix') prefix:string): any {
return this.geoService.findCitiesStartingWith(prefix);
}
@Get(':prefix/:state')
findByPrefixAndState(@Param('prefix') prefix:string,@Param('state') state:string): any {
return this.geoService.findCitiesStartingWith(prefix,state);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { GeoController } from './geo.controller.js';
import { GeoService } from './geo.service.js';
@Module({
controllers: [GeoController],
providers: [GeoService]
})
export class GeoModule {}

View File

@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { readFileSync } from 'fs';
import path, { join } from 'path';
import { City, Geo, State } from 'src/models/server.model.js';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@Injectable()
export class GeoService {
geo:Geo;
constructor() {
this.loadGeo();
}
private loadGeo(): void {
const filePath = join(__dirname,'../..', 'assets', 'geo.json');
const rawData = readFileSync(filePath, 'utf8');
this.geo = JSON.parse(rawData);
}
findCitiesStartingWith( prefix: string, state?:string): { city: string; state: string; state_code: string }[] {
const result: { city: string; state: string; state_code: string }[] = [];
this.geo.states.forEach((state: State) => {
state.cities.forEach((city: City) => {
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
result.push({
city: city.name,
state: state.name,
state_code: state.state_code
});
}
});
});
return state ? result.filter(e=>e.state_code.toLowerCase()===state.toLowerCase()) :result;
}
}

View File

@@ -0,0 +1,55 @@
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 { Logger } from 'winston';
import { FileService } from '../file/file.service.js';
import { ListingsService } from '../listings/listings.service.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
@Controller('image')
export class ImageController {
constructor(
private fileService: FileService,
private listingService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
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')
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
await this.listingService.deleteImage(imagePath, serial, imagename);
}
// ############
// Profile
// ############
@Post('uploadProfile/:email')
@UseInterceptors(FileInterceptor('file'))
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
await this.fileService.storeProfilePicture(file, adjustedEmail);
}
@Delete('profile/:email/')
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
}
// ############
// Logo
// ############
@Post('uploadCompanyLogo/:email')
@UseInterceptors(FileInterceptor('file'))
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
await this.fileService.storeCompanyLogo(file, adjustedEmail);
}
@Delete('logo/:email/')
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { ImageController } from './image.controller.js';
import { ImageService } from './image.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 { ListingsModule } from '../listings/listings.module.js';
@Module({
imports: [ListingsModule],
controllers: [ImageController],
providers: [ImageService,FileService,SelectOptionsService]
})
export class ImageModule {}

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class AccountService {} export class ImageService {}

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

@@ -0,0 +1,21 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } 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 { Logger } from 'winston';
import { UserService } from '../user/user.service.js';
@Controller('listings/professionals_brokers')
export class BrokerListingsController {
constructor(private readonly userService:UserService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
}
@Post('search')
find(@Body() criteria: any): any {
return this.userService.findUser(criteria);
}
}

View File

@@ -0,0 +1,52 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-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')
export class BusinessListingsController {
constructor(
private readonly listingsService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
@UseGuards(OptionalJwtAuthGuard)
@Get(':id')
findById(@Request() req, @Param('id') id: string): any {
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
}
@UseGuards(OptionalJwtAuthGuard)
@Get('user/:userid')
findByUserId(@Request() req, @Param('userid') userid: string): any {
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
}
@UseGuards(OptionalJwtAuthGuard)
@Post('search')
find(@Request() req, @Body() criteria: ListingCriteria): any {
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
}
@Post()
create(@Body() listing: any) {
this.logger.info(`Save Listing`);
return this.listingsService.createListing(listing, businesses);
}
@Put()
update(@Body() listing: any) {
this.logger.info(`Save Listing`);
return this.listingsService.updateBusinessListing(listing.id, listing);
}
@Delete(':id')
deleteById(@Param('id') id: string) {
this.listingsService.deleteListing(id, businesses);
}
@Get('states/all')
getStates(): any {
return this.listingsService.getStates(businesses);
}
}

View File

@@ -0,0 +1,54 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { CommercialPropertyListing } from 'src/models/db.model.js';
import { Logger } from 'winston';
import { commercials } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.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')
export class CommercialPropertyListingsController {
constructor(
private readonly listingsService: ListingsService,
private fileService: FileService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
@UseGuards(OptionalJwtAuthGuard)
@Get(':id')
findById(@Request() req, @Param('id') id: string): any {
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')
async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
}
@Get('states/all')
getStates(): any {
return this.listingsService.getStates(commercials);
}
@Post()
async create(@Body() listing: any) {
this.logger.info(`Save Listing`);
return await this.listingsService.createListing(listing, commercials);
}
@Put()
async update(@Body() listing: any) {
this.logger.info(`Save Listing`);
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
}
@Delete(':id/:imagePath')
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
this.listingsService.deleteListing(id, commercials);
this.fileService.deleteDirectoryIfExists(imagePath);
}
}

View File

@@ -1,59 +0,0 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
import { FileService } from '../file/file.service.js';
import { convertStringToNullUndefined } from '../utils.js';
import { RedisService } from '../redis/redis.service.js';
import { ListingsService } from './listings.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
@Controller('listings')
export class ListingsController {
// private readonly logger = new Logger(ListingsController.name);
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
}
@Get()
findAll(): any {
return this.listingsService.getAllListings();
}
@Get(':id')
findById(@Param('id') id:string): any {
return this.listingsService.getListingById(id);
}
// @Get(':type/:location/:minPrice/:maxPrice/:realEstateChecked')
// find(@Param('type') type:string,@Param('location') location:string,@Param('minPrice') minPrice:string,@Param('maxPrice') maxPrice:string,@Param('realEstateChecked') realEstateChecked:boolean): any {
// return this.listingsService.find(type,location,minPrice,maxPrice,realEstateChecked);
// }
@Post('search')
find(@Body() criteria: any): any {
return this.listingsService.find(criteria);
}
/**
* @param listing updates a new listing
*/
@Put(':id')
updateById(@Param('id') id:string, @Body() listing: any){
this.logger.info(`Update by ID: ${id}`);
this.listingsService.setListing(listing,id)
}
/**
* @param listing creates a new listing
*/
@Post()
create(@Body() listing: any){
this.logger.info(`Create Listing`);
this.listingsService.setListing(listing)
}
/**
* @param id deletes a listing
*/
@Delete(':id')
deleteById(@Param('id') id:string){
this.listingsService.deleteListing(id)
}
}

View File

@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module.js';
import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.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({
imports: [DrizzleModule, AuthModule],
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
providers: [ListingsService, FileService, UserService],
exports: [ListingsService],
})
export class ListingsModule {}

View File

@@ -1,86 +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';
InvestmentsListing,
ListingCriteria,
ProfessionalsBrokersListing,
} from '../models/main.model.js';
import { LISTINGS, RedisService } from '../redis/redis.service.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 * as schema from '../drizzle/schema.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 {
// private readonly logger = new Logger(ListingsService.name); constructor(
constructor(private redisService: RedisService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
async setListing( private fileService: FileService,
value: BusinessListing | ProfessionalsBrokersListing | InvestmentsListing, ) {}
id?: string, private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
) { const conditions = [];
if (!id) { if (criteria.type) {
id = await this.redisService.getId(LISTINGS); conditions.push(eq(table.type, criteria.type));
value.id = id;
this.logger.info(`No ID - creating new one:${id}`)
} else {
this.logger.info(`ID available:${id}`)
} }
this.redisService.setJson(id, value); if (criteria.state) {
conditions.push(eq(table.state, criteria.state));
}
if (criteria.minPrice) {
conditions.push(gte(table.price, criteria.minPrice));
}
if (criteria.maxPrice) {
conditions.push(lte(table.price, criteria.maxPrice));
}
if (criteria.realEstateChecked) {
conditions.push(eq(businesses.realEstateIncluded, true));
}
if (criteria.title) {
conditions.push(ilike(table.title, `%${criteria.title}%`));
}
return conditions;
}
// ##############################################################
// 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))));
}
const [data, total] = await Promise.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 findBusinessListings(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, businesses, user);
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(or(eq(businesses.draft, false), eq(businesses.imageName, emailToDirName(user?.username))));
}
const [data, total] = await Promise.all([
this.conn
.select()
.from(businesses)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(businesses)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
}
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
let result = await this.conn
.select()
.from(commercials)
.where(and(sql`${commercials.id} = ${id}`));
result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return result[0] as CommercialPropertyListing;
}
async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
let result = await this.conn
.select()
.from(businesses)
.where(and(sql`${businesses.id} = ${id}`));
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return result[0] as BusinessListing;
}
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
const conditions = [];
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(commercials.draft, true));
}
return (await this.conn
.select()
.from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
}
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const conditions = [];
conditions.push(eq(businesses.imageName, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(businesses.draft, true));
}
return (await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as CommercialPropertyListing[];
}
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
const result = await this.conn
.select()
.from(commercials)
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
return result[0] as CommercialPropertyListing;
}
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
data.created = new Date();
data.updated = new Date();
const [createdListing] = await this.conn.insert(table).values(data).returning();
return createdListing as BusinessListing | CommercialPropertyListing;
} }
async getListingById(id: string) { async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
return await this.redisService.getJson(id, LISTINGS); data.updated = new Date();
data.created = new Date(data.created);
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
if (difference.length > 0) {
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
data.imageOrder = imageOrder;
} }
deleteListing(id: string){ const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
this.redisService.delete(id); return updateListing as BusinessListing | CommercialPropertyListing;
this.logger.info(`delete listing with ID:${id}`)
} }
async getAllListings(start?: number, end?: number) { async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
const searchResult = await this.redisService.search(LISTINGS, '*'); data.updated = new Date();
const listings = searchResult.slice(1).reduce((acc, item, index, array) => { data.created = new Date(data.created);
// Jedes zweite Element (beginnend mit dem ersten) ist ein JSON-String des Listings const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
if (index % 2 === 1) { return updateListing as BusinessListing | CommercialPropertyListing;
try { }
const listing = JSON.parse(item[1]); // Parsen des JSON-Strings async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
acc.push(listing); await this.conn.delete(table).where(eq(table.id, id));
} catch (error) { }
console.error('Fehler beim Parsen des JSON-Strings: ', error); async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
return await this.conn
.select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
.from(table)
.groupBy(sql`${table.state}`)
.orderBy(sql`count desc`);
}
// ##############################################################
// Images for commercial Properties
// ##############################################################
async deleteImage(imagePath: string, serial: string, name: string) {
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) {
listing.imageOrder.splice(index, 1);
await this.updateCommercialPropertyListing(listing.id, listing);
} }
} }
return acc; async addImage(imagePath: string, serial: string, imagename: string) {
}, []); const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
return listings; listing.imageOrder.push(imagename);
} await this.updateCommercialPropertyListing(listing.id, listing);
//criteria.type,criteria.location,criteria.minPrice,criteria.maxPrice,criteria.realEstateChecked,criteria.listingsCategory
//async find(type:string,location:string,minPrice:string,maxPrice:string,realEstateChecked:boolean,listingsCategory:string): Promise<any> {
async find(criteria:ListingCriteria): Promise<any> {
let listings = await this.getAllListings();
listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory);
if (convertStringToNullUndefined(criteria.type)){
console.log(criteria.type);
listings=listings.filter(l=>l.type===criteria.type);
}
if (convertStringToNullUndefined(criteria.location)){
console.log(criteria.location);
listings=listings.filter(l=>l.location===criteria.location);
}
if (convertStringToNullUndefined(criteria.minPrice)){
console.log(criteria.minPrice);
listings=listings.filter(l=>l.price>=Number(criteria.minPrice));
}
if (convertStringToNullUndefined(criteria.maxPrice)){
console.log(criteria.maxPrice);
listings=listings.filter(l=>l.price<=Number(criteria.maxPrice));
}
if (convertStringToNullUndefined(criteria.realEstateChecked)){
console.log(criteria.realEstateChecked);
listings=listings.filter(l=>l.realEstateIncluded);
}
if (convertStringToNullUndefined(criteria.category)){
console.log(criteria.category);
listings=listings.filter(l=>l.category===criteria.category);
}
return listings
} }
} }

View File

@@ -0,0 +1,22 @@
import { Controller, Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { ListingsService } from './listings.service.js';
@Controller('listings/undefined')
export class UnknownListingsController {
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) {
// return result;
// } else {
// return await this.listingsService.findById(id, commercials);
// }
// }
}

View File

@@ -1,14 +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 { MailInfo } from '../models/server.model.js';
@Controller('mail') @Controller('mail')
export class MailController { export class MailController {
constructor(private mailService:MailService){ constructor(private mailService: MailService) {}
@Post()
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
if (mailInfo.listing) {
return this.mailService.sendInquiry(mailInfo);
} else {
return this.mailService.sendRequest(mailInfo);
} }
@Post(':id')
sendEMail(@Param('id') id:string,@Body() mailInfo: MailInfo): any {
return this.mailService.sendInquiry(id,mailInfo);
} }
} }

View File

@@ -1,25 +1,30 @@
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: 'smtp.gmail.com', host: 'email-smtp.us-east-2.amazonaws.com',
secure: true, secure: false,
port: 587,
auth: { auth: {
user: 'andreas.knuth@gmail.com', user: 'AKIAU6GDWVAQ2QNFLNWN',
pass: 'ksnh xjae dqbv xana', pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
}, },
}, },
defaults: { defaults: {
@@ -34,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,24 +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 { MailInfo } from '../models/server.model.js'; import { fileURLToPath } from 'url';
import { User } from 'src/models/main.model.js'; 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(userId:string,mailInfo: MailInfo) { async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
const user = await this.authService.getUser(userId) as User; //const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser;
const user = await this.userService.getUserByMail(mailInfo.email);
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'),
context: {
// ✏️ filling curly brackets with content
name: user.firstname, name: user.firstname,
inquiry:mailInfo.sender.comments 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,
}, },
}); });
} }
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,11 +1,64 @@
export interface MailInfo { import { Entity } from "redis-om";
sender:Sender; export interface Geo {
userId:string; id: number;
} name: string;
export interface Sender { iso3: string;
name:string; iso2: string;
email:string; numeric_code: string;
phoneNumber:string; phone_code: string;
state:string; capital: string;
comments:string; currency: string;
currency_name: string;
currency_symbol: string;
tld: string;
native: string;
region: string;
region_id: string;
subregion: string;
subregion_id: string;
nationality: string;
timezones: Timezone[];
translations: Translations;
latitude: string;
longitude: string;
emoji: string;
emojiU: string;
states: State[];
}
export interface State {
id: number;
name: string;
state_code: string;
latitude: string;
longitude: string;
type: string;
cities: City[];
}
export interface City {
id: number;
name: string;
latitude: string;
longitude: string;
}
export interface Translations {
kr: string;
'pt-BR': string;
pt: string;
nl: string;
hr: string;
fa: string;
de: string;
es: string;
fr: string;
ja: string;
it: string;
cn: string;
tr: string;
}
export interface Timezone {
zoneName: string;
gmtOffset: number;
gmtOffsetName: string;
abbreviation: string;
tzName: string;
} }

View File

@@ -1,18 +0,0 @@
import { Body, Controller, Get, HttpStatus, Param, Post, Res } from '@nestjs/common';
import { Response } from 'express';
import { RedisService } from './redis.service.js';
@Controller('redis')
export class RedisController {
constructor(private redisService:RedisService){}
// @Get(':id')
// getById(@Param('id') id:string){
// return this.redisService.getListingById(id);
// }
// @Post(':id')
// updateById(@Body() listing: any){
// this.redisService.setListing(listing);
// }
}

View File

@@ -1,16 +0,0 @@
// redis.module.ts
import { Module } from '@nestjs/common';
@Module({
providers: [RedisService],
exports: [RedisService],
controllers: [RedisController],
})
export class RedisModule {}
// redis.service.ts
import { Injectable } from '@nestjs/common';
import { RedisService } from './redis.service.js';
import { RedisController } from './redis.controller.js';

View File

@@ -1,39 +0,0 @@
// redis.service.ts
import { Injectable } from '@nestjs/common';
import { Redis } from 'ioredis';
import { BusinessListing, InvestmentsListing,ProfessionalsBrokersListing } from '../models/main.model.js';
export const LISTINGS = 'LISTINGS';
export const SUBSCRIPTIONS = 'SUBSCRIPTIONS';
export const USERS = 'USERS'
@Injectable()
export class RedisService {
private redis = new Redis(); // Verbindungsparameter nach Bedarf anpassen
// ######################################
// general methods
// ######################################
async setJson(id: string, value: any): Promise<void> {
await this.redis.call("JSON.SET", `${LISTINGS}:${id}`, "$", JSON.stringify(value));
}
async delete(id: string): Promise<void> {
await this.redis.del(`${LISTINGS}:${id}`);
}
async getJson(id: string, prefix:string): Promise<any> {
const result:string = await this.redis.call("JSON.GET", `${prefix}:${id}`) as string;
return JSON.parse(result);
}
async getId(prefix:'LISTINGS'|'SUBSCRIPTIONS'|'USERS'):Promise<string>{
const counter = await this.redis.call("INCR",`${prefix}_ID_COUNTER`) as number;
return counter.toString().padStart(15, '0')
}
async search(prefix:'LISTINGS'|'SUBSCRIPTIONS'|'USERS',clause:string):Promise<any>{
const result = await this.redis.call(`FT.SEARCH`, `${prefix}_INDEX`, `${clause}`, 'LIMIT', 0, 200);
return result;
}
}

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,15 +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,
};
} }
} }

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SelectOptionsController } from './select-options.controller.js';
import { SelectOptionsService } from './select-options.service.js';
@Module({
controllers: [SelectOptionsController],
providers: [SelectOptionsService]
})
export class SelectOptionsModule {}

View File

@@ -1,24 +1,32 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { KeyValue, KeyValueStyle } from '../models/main.model.js'; 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> = [
{ 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: '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: '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: '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' },
@@ -29,74 +37,76 @@ export class SelectOptionsService {
]; ];
public listingCategories: Array<KeyValue> = [ public listingCategories: Array<KeyValue> = [
{ name: 'Business', value: 'business' }, { name: 'Business', value: 'business' },
{ name: 'Professionals/Brokers Directory', value: 'professionals_brokers' }, { name: 'Commercial Property', value: 'commercialProperty' },
{ name: 'Investment Property', value: 'investment' }, ];
] public customerTypes: Array<KeyValue> = [
public categories: Array<KeyValueStyle> = [ { name: 'Buyer', value: 'buyer' },
{ name: 'Broker', value: 'broker', icon:'pi-image',bgColorClass:'bg-green-100',textColorClass:'text-green-600' }, { name: 'Broker', value: 'broker' },
{ name: 'Professional', value: 'professional', icon:'pi-globe',bgColorClass:'bg-yellow-100',textColorClass:'text-yellow-600' }, { name: 'Professional', value: 'professional' },
] ];
public gender: Array<KeyValue> = [
{ name: 'Male', value: 'male' },
{ 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

@@ -0,0 +1,65 @@
import { Body, Controller, Get, Inject, Param, Post, Query } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { User } from 'src/models/db.model.js';
import { Logger } from 'winston';
import { FileService } from '../file/file.service.js';
import { Subscription } from '../models/main.model.js';
import { UserService } from './user.service.js';
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
private fileService: FileService,
@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')
findById(@Param('id') id: string): any {
this.logger.info(`Searching for user with ID: ${id}`);
const user = this.userService.getUserById(id);
this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user;
}
@Post()
save(@Body() user: any): Promise<User> {
this.logger.info(`Saving user: ${JSON.stringify(user)}`);
const savedUser = this.userService.saveUser(user);
this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);
return savedUser;
}
@Post('search')
find(@Body() criteria: any): any {
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
const foundUsers = this.userService.findUser(criteria);
this.logger.info(`Found users: ${JSON.stringify(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

@@ -0,0 +1,12 @@
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 { UserService } from './user.service.js';
@Module({
imports: [DrizzleModule],
controllers: [UserController],
providers: [UserService, FileService],
})
export class UserModule {}

View File

@@ -0,0 +1,87 @@
import { Inject, Injectable } from '@nestjs/common';
import { and, eq, ilike, or, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js';
import { PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js';
import { User } from '../models/db.model.js';
import { ListingCriteria, emailToDirName } from '../models/main.model.js';
@Injectable()
export class UserService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService: FileService,
) {}
private getConditions(criteria: ListingCriteria): any[] {
const conditions = [];
if (criteria.state) {
//conditions.push(sql`EXISTS (SELECT 1 FROM unnest(users."areasServed") AS area WHERE area LIKE '%' || ${criteria.state} || '%')`);
conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: criteria.state }])}`);
}
if (criteria.name) {
conditions.push(or(ilike(schema.users.firstname, `%${criteria.name}%`), ilike(schema.users.lastname, `%${criteria.name}%`)));
}
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;
}
async getUserById(id: string) {
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 saveUser(user: any): Promise<User> {
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;
}
}

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