Compare commits

..

No commits in common. "master" and "tailwind" have entirely different histories.

262 changed files with 40281 additions and 10822 deletions

195
README.md
View File

@ -1,195 +0,0 @@
# bizmatch-project
Monorepo bestehend aus **Client** (`bizmatch-project/bizmatch`) und **Server/API** (`bizmatch-project/bizmatch-server`). Diese README führt dich vom frischen Clone bis zum laufenden System mit Produktivdaten im lokalen Dev-Setup.
---
## Voraussetzungen
- **Node.js** ≥ 20.x (empfohlen LTS) und **npm**
- **Docker** ≥ 24.x und **Docker Compose**
- Netzwerkzugriff auf die lokalen Ports (Standard: App 3001, Postgres 5433)
> **Hinweis zu Container-Namen/Ports**
> In Beispielen wird der DB-Container als `bizmatchdb` angesprochen. Falls deine Compose andere Namen/Ports nutzt (z.B. `bizmatchdb-prod` oder Ports 5433/3001), passe die Befehle entsprechend an.
---
## Repository-Struktur (Auszug)
```
bizmatch-project/
├─ bizmatch/ # Client (Angular/React/…)
├─ bizmatch-server/ # Server (NestJS + Postgres via Docker)
│ ├─ docker-compose.yml
│ ├─ env.prod # Umgebungsvariablen (Beispiel)
│ ├─ bizmatchdb-data-prod/ # (Volume-Pfad für Postgres-Daten)
│ └─ initdb/ # (optional: SQL-Skripte für Erstinitialisierung)
└─ README.md
```
---
## 1) Client starten (Ordner `bizmatch`)
```bash
cd ~/git/bizmatch-project/bizmatch
npm install
npm start
```
- Der Client startet im Dev-Modus (Standardport: meist `http://localhost:4200` oder projektspezifisch; siehe `package.json`).
- API-URL ggf. in den Client-Env-Dateien anpassen (z.B. `environment.ts`).
---
## 2) Server & Datenbank starten (Ordner `bizmatch-server`)
### 2.1 .env-Datei anlegen
Lege im Ordner `bizmatch-server` eine `.env` (oder `env.prod`) mit folgenden **Beispiel-/Dummy-Werten** an:
```
POSTGRES_DB=bizmatch
POSTGRES_USER=bizmatch
POSTGRES_PASSWORD=qG5LZhL7Y3
DATABASE_URL=postgresql://bizmatch:qG5LZhL7Y3@postgres:5432/bizmatch
OPENAI_API_KEY=sk-proj-3PVgp1dMTxnigr4nxgg
```
> **Wichtig:** Wenn du `DATABASE_URL` verwendest und dein Passwort Sonderzeichen wie `@ : / % # ?` enthält, **URL-encoden** (z.B. `@``%40`). Alternativ nur die Einzel-Variablen `POSTGRES_*` in der App verwenden.
### 2.2 Docker-Services starten
```bash
cd ~/git/bizmatch-project/bizmatch-server
# Erststart/Neustart der Services
docker compose up -d
```
- Der Server-Container baut die App (NestJS) und startet auf Port **3001** (Host), intern **3000** (Container), sofern so in `docker-compose.yml` konfiguriert.
- Postgres läuft im Container auf **5432**; per Port-Mapping meist auf **5433** am Host erreichbar (siehe `docker-compose.yml`).
> Warte nach dem Start, bis in den DB-Logs „database system is ready to accept connections“ erscheint:
>
> ```bash
> docker logs -f bizmatchdb
> ```
---
## 3) Produktiv-Dump lokal importieren
Falls du einen Dump aus der Produktion hast (Datei `prod.dump`), kannst du ihn in deine lokale DB importieren.
### 3.1 Dump in den DB-Container kopieren
```bash
# im Ordner bizmatch-server
docker cp prod.dump bizmatchdb:/tmp/prod.dump
```
> **Container-Name:** Falls dein DB-Container anders heißt (z.B. `bizmatchdb-prod`), ersetze den Namen im Befehl entsprechend.
### 3.2 Restore ausführen
```bash
docker exec -it bizmatchdb \
sh -c 'pg_restore -U "$POSTGRES_USER" --clean --no-owner -d "$POSTGRES_DB" /tmp/prod.dump'
```
- `--clean` löscht vorhandene Objekte vor dem Einspielen
- `--no-owner` ignoriert Besitzer/Role-Bindungen (praktisch für Dev)
### 3.3 Smoke-Test: DB erreichbar?
```bash
# Ping/Verbindung testen (pSQL muss im Container verfügbar sein)
docker exec -it bizmatchdb \
sh -lc 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h /var/run/postgresql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select current_user, now();"'
```
---
## 4) Häufige Probleme & Lösungen
### 4.1 `password authentication failed for user "bizmatch"`
- Prüfe, ob die Passwortänderung **in der DB** erfolgt ist (Env-Änderung allein genügt nicht, wenn das Volume existiert).
- Passwort in Postgres setzen:
```bash
docker exec -u postgres -it bizmatchdb \
psql -d postgres -c "ALTER ROLE bizmatch WITH LOGIN PASSWORD 'NEUES_PWD';"
```
- App-Umgebung (`.env`) anpassen und App neu starten:
```bash
docker compose restart app
```
- Bei Nutzung von `DATABASE_URL`: Sonderzeichen **URL-encoden**.
### 4.2 Container-Hostnamen stimmen nicht
- Innerhalb des Compose-Netzwerks ist der **Service-Name** der Host (z.B. `postgres` oder `postgres-prod`). Achte darauf, dass `DB_HOST`/`DATABASE_URL` dazu passen.
### 4.3 Dump/Restore vs. Datenverzeichnis-Kopie
- **Empfehlung:** `pg_dump/pg_restore` für Prod→Dev.
- Ganze Datenverzeichnisse (Volume) nur **bei gestoppter** DB und **identischer Postgres-Major-Version** kopieren.
### 4.4 Ports
- API nicht erreichbar? Prüfe Port-Mapping in `docker-compose.yml` (z.B. `3001:3000`) und Firewall.
- Postgres-Hostport (z.B. `5433`) gegen Client-Konfiguration prüfen.
---
## 5) Nützliche Befehle (Cheatsheet)
```bash
# Compose starten/stoppen
cd ~/git/bizmatch-project/bizmatch-server
docker compose up -d
docker compose stop
# Logs
docker logs -f bizmatchdb
docker logs -f bizmatch-app
# Shell in Container
docker exec -it bizmatchdb sh
# Datenbankbenutzer-Passwort ändern
docker exec -u postgres -it bizmatchdb \
psql -d postgres -c "ALTER ROLE bizmatch WITH LOGIN PASSWORD 'NEUES_PWD';"
# Dump aus laufender DB (vom Host, falls Port veröffentlicht ist)
PGPASSWORD="$POSTGRES_PASSWORD" \
pg_dump -h 127.0.0.1 -p 5433 -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-F c -Z 9 -f ./prod.dump
```
---
## 6) Sicherheit & Datenschutz
- Lege **keine echten Secrets** (API-Keys, Prod-Passwörter) im Repo ab. Nutze `.env`-Dateien außerhalb der Versionskontrolle oder einen Secrets-Manager.
- Bei Produktivdaten in Dev: **Anonymisierung** (Masking) für personenbezogene Daten erwägen.
---
## 7) Erweiterungen (optional)
- **Init-Skripte**: Lege SQL-Dateien in `bizmatch-server/initdb/` ab, um beim Erststart Benutzer/Schema anzulegen.
- **Multi-Stage Dockerfile** für den App-Container (schnellere, reproduzierbare Builds ohne devDependencies).
- **Makefile/Skripte** für häufige Tasks (z.B. `make db-backup`, `make db-restore`).
---
## 8) Support
Bei Fragen zu Setup, Dumps oder Container-Namen/Ports: Logs und Compose-Datei prüfen, anschließend die oben beschriebenen Tests (DNS/Ports, psql) durchführen. Anschließend Issue/Notiz anlegen mit Logs & `docker-compose.yml`-Ausschnitt.

View File

@ -2,7 +2,6 @@
/dist /dist
/node_modules /node_modules
/build /build
/data
# Logs # Logs
logs logs
@ -58,12 +57,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pictures pictures
pictures_base pictures_base
pictures_
src/*.js src/*.js
bun.lockb bun.lockb
#drizzle migrations
src/drizzle/migrations
importlog.txt

View File

@ -5,8 +5,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Debug Nest Framework", "name": "Debug Nest Framework",
//"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"runtimeExecutable": "/home/aknuth/.nvm/versions/node/v22.14.0/bin/npm",
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"], "runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
"autoAttachChildProcesses": true, "autoAttachChildProcesses": true,
"restart": true, "restart": true,
@ -14,20 +13,16 @@
"stopOnEntry": false, "stopOnEntry": false,
"console": "integratedTerminal", "console": "integratedTerminal",
"env": { "env": {
"HOST_NAME": "localhost", "HOST_NAME": "localhost"
"FIREBASE_PROJECT_ID": "bizmatch-net",
"FIREBASE_CLIENT_EMAIL": "firebase-adminsdk-fbsvc@bizmatch-net.iam.gserviceaccount.com",
"FIREBASE_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsOlDmhG0zi1zh\nlvobM8yAmLDR3P0F7mHcLyAga2rZm9MnPiGcmkoqRtDnxpZXio36PiyEgdKyhJFK\nP+jPJx1Zo/Ko9vb983oCGcz6MWgRKFXwLT4UJXjwjBdNDe/gcl52c+JJtZJR4bwD\n/bBgkoLzU9lF97pJoQypkSXytyxea6yrS2oEDs7SjW7z9JGFsoxFrt7zbMRb8tIs\nyCWe4I9YSgjSrwOw2uXpdrV0qjDkjx1TokuVJHDH9Vi8XhXDBx9y87Ja0hBoYDE9\nJJRLAa70qHQ9ytfdH/H0kucptC1JkdYGmLQHbohoPDuTU/C85JZvqIitwJ4YEH6Y\nfd+gEe5TAgMBAAECggEALrKDI/WNDFhBn1MJzl1dmhKMguKJ4lVPyF0ot1GYv5bu\nCipg/66f5FWeJ/Hi6qqBM3QvKuBuagPixwCMFbrTzO3UijaoIpQlJTOsrbu+rURE\nBOKnfdvpLkO1v6lDPJaWAUULepPWMAhmK6jZ7V1cTzCRbVSteHBH2CQoZ2Z+C71w\nyvzAIr6JRSg4mYbtHrQCXx9odPCRTdiRvxu5QtihiZGFSXnkTfhDNL1DKff7XHKF\nbOaDPumGtE7ypXr+0qyefg8xeTmXxdI4lPdqxd8XTpLFdMU8nW+/sEjdR40G8ikf\nt6nwyMh01YMMNi88t7ZoDvhpLALb4OqHBhDmyMdOWQKBgQDm5I0cqYX18jypC32G\nUhOdOou6IaZlVDNztZUhFPHPrP0P5Qg1PE5E5YybV7GVNXWiNwI/MPPF0JBce/Ie\ngJoXnuQ9kLh7cNZ432Jhz/Nmhytr6RGxoykAMT1fCuVLsTCfuK4e/aDAgVFJ84gS\nsB3TA62t2hak2MMntKoAQeDwWwKBgQC+9K+MRI/Vj1Xl7jwJ+adRQIvOssVz74ZE\nRYwIDZNRdk/c7c63WVHXASCRZbroGvqJgVfnmtwR6XJTnW3tkYqKUl5W9E+FSVbf\ng4aZs1oaVMA/IirVlRbJ4oCT+nDxPPuJ3ceJ4mBcODO82zXaC6pSFCvkpz9k9lc3\nUPlTLk1baQKBgFMbLqODbSFSeH0MErlXL5InMYXkeMT+IqriT/QhWsw6Yrfm4yZu\nN2nbCdocHWIsZNPnYtql3whzgpKXVlWeSlh4K4TxY0WjHr9RAFNeiyh7PKjRsjmz\nFZ3pG0LrZA7zjyHeUmX7OnIv2bd5fZ/kXkfGiiwKVJ4vG0deYtZG4BUDAoGBAJbI\nFRn4RW8HiHdPv37M8E5bXknvpbRfDTE5jVIKjioD9xnneZQTZmkUjcfhgU2nh+8t\n/+B0ypMmN81IgTXW94MzeSTGM0h22a8SZyVUlrA1/bucWiBeYik1vfubBLWoRqLd\nSaNZ6mbHRis5GPO8xFedb+9UFN2/Gq0mNkl1RUYJAoGBALqTxfdr4MXnG6Nhy22V\nWqui9nsHE5RMIvGYBnnq9Kqt8tUEkxB52YkBilx43q/TY4DRMDOeJk2krEbSN3AO\nguTE6BmZacamrt1HIdSAmJ1RktlVDRgIHXMBkBIumCsTCuXaZ+aEjuLOXJDIsIHZ\nEA9ftLrt1h1u+7QPI+E11Fmx\n-----END PRIVATE KEY-----"
} }
// "preLaunchTask": "Start Stripe Listener"
}, },
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch import from exported with tsx", "name": "Launch TypeScript file with tsx",
"runtimeExecutable": "npx", "runtimeExecutable": "npx",
"runtimeArgs": ["tsx", "--inspect"], "runtimeArgs": ["tsx", "--inspect"],
"args": ["${workspaceFolder}/src/drizzle/importFromExported.ts"], "args": ["${workspaceFolder}/src/drizzle/import.ts"],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"], "outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"],
"sourceMaps": true, "sourceMaps": true,
@ -65,30 +60,5 @@
"sourceMaps": true, "sourceMaps": true,
"smartStep": true "smartStep": true
} }
],
"tasks": [
{
"label": "Start Stripe Listener",
"type": "shell",
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
"isBackground": true,
"problemMatcher": [
{
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "."
}
}
]
}
] ]
} }

View File

@ -1,31 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Stripe Listener",
"type": "shell",
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
"problemMatcher": [],
"isBackground": true, // Task läuft im Hintergrund
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
},
{
"label": "Start Nest.js",
"type": "npm",
"script": "start:debug",
"isBackground": false,
"problemMatcher": [],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
}
]
}

View File

@ -1,19 +0,0 @@
# Build Stage
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Runtime Stage
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist /app/dist
COPY --from=build /app/package*.json /app/
RUN npm install --production
CMD ["node", "dist/main.js"]

239
bizmatch-server/dbschema.ts Normal file
View File

@ -0,0 +1,239 @@
/* tslint:disable */
/* eslint-disable */
/**
* AUTO-GENERATED FILE - DO NOT EDIT!
*
* This file was automatically generated by pg-to-ts v.4.1.1
* $ pg-to-ts generate -c postgresql://username:password@localhost:5432/bizmatch -t businesses -t commercials -t users -s public
*
*/
export type Json = unknown;
export type customerSubType = 'appraiser' | 'attorney' | 'broker' | 'cpa' | 'surveyor' | 'titleCompany';
export type customerType = 'buyer' | 'professional';
export type gender = 'female' | 'male';
export type listingsCategory = 'business' | 'commercialProperty';
// Table businesses
export interface Businesses {
id: string;
email: string | null;
type: string | null;
title: string | null;
description: string | null;
city: string | null;
state: string | null;
zipCode: number | null;
county: string | null;
price: number | null;
favoritesForUser: string[] | null;
draft: boolean | null;
listingsCategory: listingsCategory | null;
realEstateIncluded: boolean | null;
leasedLocation: boolean | null;
franchiseResale: boolean | null;
salesRevenue: number | null;
cashFlow: number | null;
supportAndTraining: string | null;
employees: number | null;
established: number | null;
internalListingNumber: number | null;
reasonForSale: string | null;
brokerLicencing: string | null;
internals: string | null;
imageName: string | null;
created: Date | null;
updated: Date | null;
visits: number | null;
lastVisit: Date | null;
latitude: number | null;
longitude: number | null;
}
export interface BusinessesInput {
id?: string;
email?: string | null;
type?: string | null;
title?: string | null;
description?: string | null;
city?: string | null;
state?: string | null;
zipCode?: number | null;
county?: string | null;
price?: number | null;
favoritesForUser?: string[] | null;
draft?: boolean | null;
listingsCategory?: listingsCategory | null;
realEstateIncluded?: boolean | null;
leasedLocation?: boolean | null;
franchiseResale?: boolean | null;
salesRevenue?: number | null;
cashFlow?: number | null;
supportAndTraining?: string | null;
employees?: number | null;
established?: number | null;
internalListingNumber?: number | null;
reasonForSale?: string | null;
brokerLicencing?: string | null;
internals?: string | null;
imageName?: string | null;
created?: Date | null;
updated?: Date | null;
visits?: number | null;
lastVisit?: Date | null;
latitude?: number | null;
longitude?: number | null;
}
const businesses = {
tableName: 'businesses',
columns: ['id', 'email', 'type', 'title', 'description', 'city', 'state', 'zipCode', 'county', 'price', 'favoritesForUser', 'draft', 'listingsCategory', 'realEstateIncluded', 'leasedLocation', 'franchiseResale', 'salesRevenue', 'cashFlow', 'supportAndTraining', 'employees', 'established', 'internalListingNumber', 'reasonForSale', 'brokerLicencing', 'internals', 'imageName', 'created', 'updated', 'visits', 'lastVisit', 'latitude', 'longitude'],
requiredForInsert: [],
primaryKey: 'id',
foreignKeys: { email: { table: 'users', column: 'email', $type: null as unknown as Users }, },
$type: null as unknown as Businesses,
$input: null as unknown as BusinessesInput
} as const;
// Table commercials
export interface Commercials {
id: string;
serialId: number;
email: string | null;
type: string | null;
title: string | null;
description: string | null;
city: string | null;
state: string | null;
price: number | null;
favoritesForUser: string[] | null;
listingsCategory: listingsCategory | null;
hideImage: boolean | null;
draft: boolean | null;
zipCode: number | null;
county: string | null;
imageOrder: string[] | null;
imagePath: string | null;
created: Date | null;
updated: Date | null;
visits: number | null;
lastVisit: Date | null;
latitude: number | null;
longitude: number | null;
}
export interface CommercialsInput {
id?: string;
serialId?: number;
email?: string | null;
type?: string | null;
title?: string | null;
description?: string | null;
city?: string | null;
state?: string | null;
price?: number | null;
favoritesForUser?: string[] | null;
listingsCategory?: listingsCategory | null;
hideImage?: boolean | null;
draft?: boolean | null;
zipCode?: number | null;
county?: string | null;
imageOrder?: string[] | null;
imagePath?: string | null;
created?: Date | null;
updated?: Date | null;
visits?: number | null;
lastVisit?: Date | null;
latitude?: number | null;
longitude?: number | null;
}
const commercials = {
tableName: 'commercials',
columns: ['id', 'serialId', 'email', 'type', 'title', 'description', 'city', 'state', 'price', 'favoritesForUser', 'listingsCategory', 'hideImage', 'draft', 'zipCode', 'county', 'imageOrder', 'imagePath', 'created', 'updated', 'visits', 'lastVisit', 'latitude', 'longitude'],
requiredForInsert: [],
primaryKey: 'id',
foreignKeys: { email: { table: 'users', column: 'email', $type: null as unknown as Users }, },
$type: null as unknown as Commercials,
$input: null as unknown as CommercialsInput
} as const;
// Table users
export interface Users {
id: string;
firstname: string;
lastname: string;
email: string;
phoneNumber: string | null;
description: string | null;
companyName: string | null;
companyOverview: string | null;
companyWebsite: string | null;
companyLocation: string | null;
offeredServices: string | null;
areasServed: Json | null;
hasProfile: boolean | null;
hasCompanyLogo: boolean | null;
licensedIn: Json | null;
gender: gender | null;
customerType: customerType | null;
customerSubType: customerSubType | null;
created: Date | null;
updated: Date | null;
latitude: number | null;
longitude: number | null;
}
export interface UsersInput {
id?: string;
firstname: string;
lastname: string;
email: string;
phoneNumber?: string | null;
description?: string | null;
companyName?: string | null;
companyOverview?: string | null;
companyWebsite?: string | null;
companyLocation?: string | null;
offeredServices?: string | null;
areasServed?: Json | null;
hasProfile?: boolean | null;
hasCompanyLogo?: boolean | null;
licensedIn?: Json | null;
gender?: gender | null;
customerType?: customerType | null;
customerSubType?: customerSubType | null;
created?: Date | null;
updated?: Date | null;
latitude?: number | null;
longitude?: number | null;
}
const users = {
tableName: 'users',
columns: ['id', 'firstname', 'lastname', 'email', 'phoneNumber', 'description', 'companyName', 'companyOverview', 'companyWebsite', 'companyLocation', 'offeredServices', 'areasServed', 'hasProfile', 'hasCompanyLogo', 'licensedIn', 'gender', 'customerType', 'customerSubType', 'created', 'updated', 'latitude', 'longitude'],
requiredForInsert: ['firstname', 'lastname', 'email'],
primaryKey: 'id',
foreignKeys: {},
$type: null as unknown as Users,
$input: null as unknown as UsersInput
} as const;
export interface TableTypes {
businesses: {
select: Businesses;
input: BusinessesInput;
};
commercials: {
select: Commercials;
input: CommercialsInput;
};
users: {
select: Users;
input: UsersInput;
};
}
export const tables = {
businesses,
commercials,
users,
}

View File

@ -1,45 +0,0 @@
# ~/git/bizmatch-project/bizmatch-server/docker-compose.yml
services:
app:
image: node:22-alpine
container_name: bizmatch-app
working_dir: /app
volumes:
- ./:/app # Code liegt hier direkt im Ordner der Compose
ports:
- '3001:3000' # Host 3001 -> Container 3000
env_file:
- path: ./.env
required: true
environment:
- NODE_ENV=development # Prod-Modus (vorher stand fälschlich "development")
- DATABASE_URL
# Hinweis: npm ci nutzt package-lock.json; falls nicht vorhanden, nimm "npm install"
command: sh -c "npm ci && npm run build && node dist/src/main.js"
restart: unless-stopped
depends_on:
- postgres
networks:
- bizmatch
postgres:
container_name: bizmatchdb
image: postgres:17-alpine # Version pinnen ist stabiler als "latest"
restart: unless-stopped
volumes:
- ${PWD}/bizmatchdb-data:/var/lib/postgresql/data # Daten liegen im Server-Repo
env_file:
- path: ./.env
required: true
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- '5433:5432' # Host 5433 -> Container 5432
networks:
- bizmatch
networks:
bizmatch:
external: true # einmalig anlegen: docker network create bizmatch-prod

View File

@ -3,6 +3,7 @@ export default defineConfig({
schema: './src/drizzle/schema.ts', schema: './src/drizzle/schema.ts',
out: './src/drizzle/migrations', out: './src/drizzle/migrations',
dialect: 'postgresql', dialect: 'postgresql',
// driver: 'pg',
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL, url: process.env.DATABASE_URL,
}, },

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,14 @@
"author": "", "author": "",
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"type": "module",
"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:local": "HOST_NAME=localhost node dist/src/main", "start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
"start:dev": "NODE_ENV=development node dist/src/main",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "NODE_ENV=production node dist/src/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",
@ -26,33 +26,35 @@
"generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts" "generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^2.0.2", "@nestjs-modules/mailer": "^1.10.3",
"@nestjs/common": "^11.0.11", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^4.0.0", "@nestjs/config": "^3.2.0",
"@nestjs/core": "^11.0.11", "@nestjs/core": "^10.0.0",
"@nestjs/cli": "^11.0.11", "@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^11.0.11", "@nestjs/passport": "^10.0.3",
"@types/stripe": "^8.0.417", "@nestjs/platform-express": "^10.0.0",
"body-parser": "^1.20.2", "@nestjs/serve-static": "^4.0.1",
"cls-hooked": "^4.2.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.32.0", "drizzle-orm": "^0.32.0",
"firebase": "^11.3.1",
"firebase-admin": "^13.1.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"groq-sdk": "^0.5.0", "groq-sdk": "^0.5.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"jwks-rsa": "^3.1.0",
"ky": "^1.4.0",
"nest-winston": "^1.9.4", "nest-winston": "^1.9.4",
"nestjs-cls": "^5.4.0",
"nodemailer": "^6.9.10", "nodemailer": "^6.9.10",
"nodemailer-smtp-transport": "^2.7.4", "nodemailer-smtp-transport": "^2.7.4",
"openai": "^4.52.6", "openai": "^4.52.6",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.11.5", "pg": "^8.11.5",
"pgvector": "^0.2.0", "pgvector": "^0.2.0",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sharp": "^0.33.2", "sharp": "^0.33.2",
"stripe": "^16.8.0",
"tsx": "^4.16.2", "tsx": "^4.16.2",
"urlcat": "^3.1.0", "urlcat": "^3.1.0",
"winston": "^3.11.0", "winston": "^3.11.0",
@ -61,22 +63,29 @@
"devDependencies": { "devDependencies": {
"@babel/parser": "^7.24.4", "@babel/parser": "^7.24.4",
"@babel/traverse": "^7.24.1", "@babel/traverse": "^7.24.1",
"@nestjs/cli": "^11.0.5", "@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^11.0.1", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^11.0.11", "@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/multer": "^1.4.11", "@types/multer": "^1.4.11",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/pg": "^8.11.5", "@types/pg": "^8.11.5",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"drizzle-kit": "^0.23.0", "drizzle-kit": "^0.23.0",
"esbuild-register": "^3.5.0", "esbuild-register": "^3.5.0",
"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",
"kysely-codegen": "^0.15.0", "kysely-codegen": "^0.15.0",
"nest-commander": "^3.16.1",
"pg-to-ts": "^4.1.1", "pg-to-ts": "^4.1.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",

Binary file not shown.

View File

@ -1,5 +1,5 @@
import { Body, Controller, Post } from '@nestjs/common'; import { Body, Controller, Post } from '@nestjs/common';
import { AiService } from './ai.service'; import { AiService } from './ai.service.js';
@Controller('ai') @Controller('ai')
export class AiController { export class AiController {

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AiController } from './ai.controller'; import { AiController } from './ai.controller.js';
import { AiService } from './ai.service'; import { AiService } from './ai.service.js';
@Module({ @Module({
controllers: [AiController], controllers: [AiController],

View File

@ -3,85 +3,30 @@ import Groq from 'groq-sdk';
import OpenAI from 'openai'; import OpenAI from 'openai';
import { BusinessListingCriteria } from '../models/main.model'; import { BusinessListingCriteria } from '../models/main.model';
// const businessListingCriteriaStructure = { const businessListingCriteriaStructure = {
// criteriaType: 'business | commercialProperty | broker', criteriaType: 'business | commercialProperty | broker',
// types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'", types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
// city: 'string', city: 'string',
// state: 'string', state: 'string',
// county: 'string', county: 'string',
// minPrice: 'number', minPrice: 'number',
// maxPrice: 'number', maxPrice: 'number',
// minRevenue: 'number', minRevenue: 'number',
// maxRevenue: 'number', maxRevenue: 'number',
// minCashFlow: 'number', minCashFlow: 'number',
// maxCashFlow: 'number', maxCashFlow: 'number',
// minNumberEmployees: 'number', minNumberEmployees: 'number',
// maxNumberEmployees: 'number', maxNumberEmployees: 'number',
// establishedSince: 'number', establishedSince: 'number',
// establishedUntil: 'number', establishedUntil: 'number',
// realEstateChecked: 'boolean', realEstateChecked: 'boolean',
// leasedLocation: 'boolean', leasedLocation: 'boolean',
// franchiseResale: 'boolean', franchiseResale: 'boolean',
// title: 'string', title: 'string',
// brokerName: 'string', brokerName: 'string',
// searchType: "'exact' | 'radius'", searchType: "'exact' | 'radius'",
// radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'", radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'",
// }; };
const BusinessListingCriteriaStructure = `
export interface BusinessListingCriteria {
state: string;
city: string;
searchType: 'exact' | 'radius';
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
minPrice: number;
maxPrice: number;
minRevenue: number;
maxRevenue: number;
minCashFlow: number;
maxCashFlow: number;
minNumberEmployees: number;
maxNumberEmployees: number;
establishedSince: number;
establishedUntil: number;
realEstateChecked: boolean;
leasedLocation: boolean;
franchiseResale: boolean;
//title: string;
brokerName: string;
//types:"'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
criteriaType: 'businessListings';
}
`;
const CommercialPropertyListingCriteriaStructure = `
export interface CommercialPropertyListingCriteria {
state: string;
city: string;
searchType: 'exact' | 'radius';
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
minPrice: number;
maxPrice: number;
//title: string;
//types:"'Retail'|'Land'|'Industrial'|'Office'|'Mixed Use'|'Multifamily'|'Uncategorized'"
criteriaType: 'commercialPropertyListings';
}
`;
const UserListingCriteriaStructure = `
export interface UserListingCriteria {
state: string;
city: string;
searchType: 'exact' | 'radius';
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
brokerName: string;
companyName: string;
counties: string[];
criteriaType: 'brokerListings';
}
`;
@Injectable() @Injectable()
export class AiService { export class AiService {
private readonly openai: OpenAI; private readonly openai: OpenAI;
@ -101,49 +46,42 @@ export class AiService {
const prompt = `The Search Query of the User is: "${query}"`; const prompt = `The Search Query of the User is: "${query}"`;
let response = null; let response = null;
try { try {
response = await this.openai.chat.completions.create({ // response = await this.openai.chat.completions.create({
model: 'gpt-4o-mini', // model: 'gpt-4o-mini',
//model: 'gpt-3.5-turbo', // //model: 'gpt-3.5-turbo',
max_tokens: 300, // max_tokens: 300,
// messages: [
// {
// role: 'system',
// content: `Please create unformatted JSON Object from a user input.
// The type is: ${JSON.stringify(businessListingCriteriaStructure)}.,
// If location details available please fill city, county and state as State Code`,
// },
// ],
// temperature: 0.5,
// response_format: { type: 'json_object' },
// });
response = await this.groq.chat.completions.create({
messages: [ messages: [
{ {
role: 'system', role: 'system',
content: `Please create unformatted JSON Object from a user input. content: `Please create unformatted JSON Object from a user input.
The criteriaType must be only either 'businessListings' or 'commercialPropertyListings' or 'brokerListings' !!!! The type must be: ${JSON.stringify(businessListingCriteriaStructure)}.
The format of the object (depending on your choice of criteriaType) must be either ${BusinessListingCriteriaStructure}, ${CommercialPropertyListingCriteriaStructure} or ${UserListingCriteriaStructure} !!!! If location details available please fill city, county and state as State Code`,
If location details available please fill city and state as State Code and only county if explicitly mentioned.
If you decide for searchType==='exact', please do not set the attribute radius`,
}, },
{ {
role: 'user', role: 'user',
content: prompt, content: prompt,
}, },
], ],
temperature: 0.5, model: 'llama-3.1-70b-versatile',
//model: 'llama-3.1-8b-instant',
temperature: 0.2,
max_tokens: 300,
response_format: { type: 'json_object' }, response_format: { type: 'json_object' },
}); });
// response = await this.groq.chat.completions.create({
// messages: [
// {
// role: 'system',
// content: `Please create unformatted JSON Object from a user input.
// The criteriaType must be only either 'businessListings' or 'commercialPropertyListings' or 'brokerListings' !!!!
// The format of the object (depending on your choice of criteriaType) must be either ${BusinessListingCriteriaStructure}, ${CommercialPropertyListingCriteriaStructure} or ${UserListingCriteriaStructure} !!!!
// If location details available please fill city and state as State Code and only county if explicitly mentioned.
// If you decide for searchType==='exact', please do not set the attribute radius`,
// },
// {
// role: 'user',
// content: prompt,
// },
// ],
// model: 'llama-3.3-70b-versatile',
// temperature: 0.2,
// max_tokens: 300,
// response_format: { type: 'json_object' },
// });
const generatedCriteria = JSON.parse(response.choices[0]?.message?.content); const generatedCriteria = JSON.parse(response.choices[0]?.message?.content);
return generatedCriteria; return generatedCriteria;

View File

@ -1,17 +1,19 @@
import { Controller, Get, Request, UseGuards } from '@nestjs/common'; import { Controller, Get, Request, UseGuards } from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service.js';
import { AuthService } from './auth/auth.service.js';
import { AuthGuard } from './jwt-auth/auth.guard'; import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
@Controller() @Controller()
export class AppController { export class AppController {
constructor( constructor(
private readonly appService: AppService, private readonly appService: AppService,
private authService: AuthService,
) {} ) {}
@UseGuards(AuthGuard) @UseGuards(JwtAuthGuard)
@Get() @Get()
getHello(@Request() req): string { getHello(@Request() req): string {
return req.user; return req.user;
//return 'dfgdf';
} }
} }

View File

@ -1,54 +1,62 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { MiddlewareConsumer, Module } from '@nestjs/common';
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import { AiModule } from './ai/ai.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { FileService } from './file/file.service';
import { GeoModule } from './geo/geo.module';
import { ImageModule } from './image/image.module';
import { ListingsModule } from './listings/listings.module';
import { LogController } from './log/log.controller';
import { LogModule } from './log/log.module';
import { EventModule } from './event/event.module';
import { MailModule } from './mail/mail.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { APP_INTERCEPTOR } from '@nestjs/core'; import { PassportModule } from '@nestjs/passport';
import { ClsMiddleware, ClsModule } from 'nestjs-cls'; import * as dotenv from 'dotenv';
import path from 'path'; import fs from 'fs-extra';
import { AuthService } from './auth/auth.service'; import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
import { FirebaseAdminModule } from './firebase-admin/firebase-admin.module'; import * as winston from 'winston';
import { LoggingInterceptor } from './interceptors/logging.interceptor'; import { AiModule } from './ai/ai.module.js';
import { UserInterceptor } from './interceptors/user.interceptor'; import { AppController } from './app.controller.js';
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware'; import { AppService } from './app.service.js';
import { SelectOptionsModule } from './select-options/select-options.module'; import { AuthModule } from './auth/auth.module.js';
import { UserModule } from './user/user.module'; 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 __dirname = path.dirname(__filename);
//loadEnvFiles(); function loadEnvFiles() {
console.log('Loaded environment variables:'); // Load the .env file
//console.log(JSON.stringify(process.env, null, 2)); dotenv.config();
console.log('Loaded .env file');
// Determine which additional env file to load
let envFilePath = '';
const host = process.env.HOST_NAME || '';
if (host.includes('localhost')) {
envFilePath = '.env.local';
} else if (host.includes('dev.bizmatch.net')) {
envFilePath = '.env.dev';
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
envFilePath = '.env.prod';
}
// Load the additional env file if it exists
if (fs.existsSync(envFilePath)) {
dotenv.config({ path: envFilePath });
console.log(`Loaded ${envFilePath} file`);
} else {
console.log(`No additional .env file found for HOST_NAME: ${host}`);
}
}
loadEnvFiles();
@Module({ @Module({
imports: [ imports: [
ClsModule.forRoot({ ConfigModule.forRoot({ isGlobal: true }),
global: true, // Macht den ClsService global verfügbar
middleware: { mount: true }, // Registriert automatisch die ClsMiddleware
}),
//ConfigModule.forRoot({ envFilePath: '.env' }),
ConfigModule.forRoot({
envFilePath: [path.resolve(__dirname, '..', '.env')],
}),
MailModule, MailModule,
AuthModule, AuthModule,
WinstonModule.forRoot({ WinstonModule.forRoot({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp({ winston.format.timestamp(),
format: 'YYYY-MM-DD hh:mm:ss.SSS A',
}),
winston.format.ms(), winston.format.ms(),
nestWinstonModuleUtilities.format.nestLike('Bizmatch', { nestWinstonModuleUtilities.format.nestLike('Bizmatch', {
colors: true, colors: true,
@ -65,30 +73,14 @@ console.log('Loaded environment variables:');
ListingsModule, ListingsModule,
SelectOptionsModule, SelectOptionsModule,
ImageModule, ImageModule,
PassportModule,
AiModule, AiModule,
LogModule,
// PaymentModule,
EventModule,
FirebaseAdminModule,
],
controllers: [AppController, LogController],
providers: [
AppService,
FileService,
{
provide: APP_INTERCEPTOR,
useClass: UserInterceptor, // Registriere den Interceptor global
},
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor, // Registriere den LoggingInterceptor global
},
AuthService,
], ],
controllers: [AppController],
providers: [AppService, FileService],
}) })
export class AppModule implements NestModule { export class AppModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
consumer.apply(ClsMiddleware).forRoutes('*');
consumer.apply(RequestDurationMiddleware).forRoutes('*'); consumer.apply(RequestDurationMiddleware).forRoutes('*');
} }
} }

View File

@ -1,30 +0,0 @@
{
"keys": [
{
"kid": "0NxHr10meEVrGYmGlWz_WHiTPxbuNaU6vmShQYWFBh8",
"kty": "RSA",
"alg": "RSA-OAEP",
"use": "enc",
"n": "7hzWTnW6WOrZQmeZ26fD5Fu0NvxiQP8pVfesK9MXO4R1gjGlPViGWCdUKrG9Ux6h9X6SXHOWPWZmbfmjNeK7kQOjYPS_06GQ3X19tFikdWoufZMTpAb6p9CENsIbpzX9c1JZRs1xSJ9B505NjLVp29WzhugQfQR2ctv4nLZYmo1ojGjUQMGPNO_4bMqzO_luBQGEAqnRojZzxHVp-ruNyR9DmQbPbUULrOOXfGjCeAYukZ-5UHl6pngk8b6NKdGq6E_qxNsZVStWxbeGAG5UhxSl6oaGL8R0fP9JiAtlWfubJsCtibk712MaMb59JEdr_f3R3pXN7He8brS3smPgcQ",
"e": "AQAB",
"x5c": [
"MIIClTCCAX0CBgGN9oQZDTANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjQwMjI5MjAxNjA4WhcNMzQwMjI4MjAxNzQ4WjAOMQwwCgYDVQQDDANkZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDuHNZOdbpY6tlCZ5nbp8PkW7Q2/GJA/ylV96wr0xc7hHWCMaU9WIZYJ1Qqsb1THqH1fpJcc5Y9ZmZt+aM14ruRA6Ng9L/ToZDdfX20WKR1ai59kxOkBvqn0IQ2whunNf1zUllGzXFIn0HnTk2MtWnb1bOG6BB9BHZy2/ictliajWiMaNRAwY807/hsyrM7+W4FAYQCqdGiNnPEdWn6u43JH0OZBs9tRQus45d8aMJ4Bi6Rn7lQeXqmeCTxvo0p0aroT+rE2xlVK1bFt4YAblSHFKXqhoYvxHR8/0mIC2VZ+5smwK2JuTvXYxoxvn0kR2v9/dHelc3sd7xutLeyY+BxAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAL5CFk/T8Thoi6yNRg7CSiWuehCwwzC+FfsoVQNkcq2loZYgWjO34b9fXysT0xXLJOAnw0+xvwAhbVdBwTathQb2PJST5Ei6RGIOsT2gfE91Je3BGpXnsNNDja0be1bS/uN07aa3MshkgVBOYVDe2FoK7g4zSgViMXLEzGpRdio9mIrH3KADdEAFrhiNClu19gefONT86vUvIpSCS4XJ+nSUPbNkbhe9MlvZ8TRWFMoUzuZML6Xf+FbimAv1ZBk1NWobWPtyaDFF9Lgse7LHGiKPKvBHonVMbWYf7Lk8nGA7/90WVOX5Fd2LItH/13rPNlwbspAcz/nB2groa8/DrdE="
],
"x5t": "3ZyfzL7Gn0dcNq8H8X1L0uagQMI",
"x5t#S256": "Wwu30X3ZnchcXsJHJmOHT8BLOFCH6y2TpO3hyzojhdk"
},
{
"kid": "yAfIWlA3TFvR_h112X4sJHK0kog4_4xDLkRnJnzTv98",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "xpYiq2XOtKV-xeLmFM-4sUWDpzw1UJlN9NXj833MZKsW_bwWixlsJTsB-2kfQ6mXUTbfxsuoZuWMZdQVpsWoKOPeK1Gsd8Gsoa0v2pv3uzPA8_SLqDrBNtIz9mDJc6jf-XkOdtAfPzW_aMf4TzThzIkEH5ptUde0gDKNd8je2lFo4loFJkLhOO2HZ7cLQcspXB_vNqpjAMED15GmGRizeTsA4IWC9WjGyziVvlbgQqC0MqCieT2r4dB0FZGWFwzlm-EhvyHu6G1Hw55jn5AcEHh5fke9XvTBzF6MmM_MQEDc9QWHj16ekVdQB7fxzBHbyLMr3ivQizcHAGYvemNhHw",
"e": "AQAB",
"x5c": [
"MIIClTCCAX0CBgGN9oQYYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjQwMjI5MjAxNjA4WhcNMzQwMjI4MjAxNzQ4WjAOMQwwCgYDVQQDDANkZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGliKrZc60pX7F4uYUz7ixRYOnPDVQmU301ePzfcxkqxb9vBaLGWwlOwH7aR9DqZdRNt/Gy6hm5Yxl1BWmxago494rUax3wayhrS/am/e7M8Dz9IuoOsE20jP2YMlzqN/5eQ520B8/Nb9ox/hPNOHMiQQfmm1R17SAMo13yN7aUWjiWgUmQuE47YdntwtByylcH+82qmMAwQPXkaYZGLN5OwDghYL1aMbLOJW+VuBCoLQyoKJ5Pavh0HQVkZYXDOWb4SG/Ie7obUfDnmOfkBwQeHl+R71e9MHMXoyYz8xAQNz1BYePXp6RV1AHt/HMEdvIsyveK9CLNwcAZi96Y2EfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABQaqejZ5iWybWeiK0j9iKTn5DNr8LFXdJNRk+odI5TwCtaVDTCQRrF1KKT6F6RmzQyc6xyKojtnI1mKjs+Wo8vYE483pDgoGkv7UquKeQAWbXRajbkpGKasIux7m0MgDhPGKtxoha3kI2Yi2dOFYGdRuqv35/ZD+9nfHfk03fylrf5saroOYBGW6RRpdygB14zQ5ZbXin6gVJSBuJWMiWpxzAB05llZVaHOJ7kO+402YV2/l2TJm0bc883HZuIKxh11PI20lZop9ZwctVtmwf2iFfMfQgQ5wZpV/1gEMynVypxe6OY7biQyIERX6oEFWmZIOrnytSawLyy5gCFrStY="
],
"x5t": "L27m4VtyyHlrajDI_47_mmRSP08",
"x5t#S256": "KOcIpGLNb4ZGg_G2jc6ieZC_86-QQjoaSsMDoV0RWZg"
}
]
}

View File

@ -1,139 +1,40 @@
import { Body, Controller, Get, HttpException, HttpStatus, Inject, Param, Post, Query, Req, UseGuards } from '@nestjs/common'; import { Controller, Get, Param, Put } from '@nestjs/common';
import * as admin from 'firebase-admin'; import { AuthService } from './auth.service.js';
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { LocalhostGuard } from 'src/jwt-auth/localhost.guard';
import { UserRole, UsersResponse } from 'src/models/main.model';
import { AuthService } from './auth.service';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor( constructor(private readonly authService: AuthService) {}
@Inject('FIREBASE_ADMIN')
private readonly firebaseAdmin: typeof admin,
private readonly authService: AuthService,
) {}
@Post('verify-email')
async verifyEmail(@Body('oobCode') oobCode: string, @Body('email') email: string) {
if (!oobCode || !email) {
throw new HttpException('oobCode and email are required', HttpStatus.BAD_REQUEST);
}
try {
// Step 1: Get the user by email address
const userRecord = await this.firebaseAdmin.auth().getUserByEmail(email);
if (userRecord.emailVerified) {
// Even if already verified, we'll still return a valid token
const customToken = await this.firebaseAdmin.auth().createCustomToken(userRecord.uid);
return {
message: 'Email is already verified',
token: customToken,
};
}
// Step 2: Update the user status to set emailVerified to true
await this.firebaseAdmin.auth().updateUser(userRecord.uid, {
emailVerified: true,
});
// Step 3: Generate a custom Firebase token for the user
// This token can be used on the client side to authenticate with Firebase
const customToken = await this.firebaseAdmin.auth().createCustomToken(userRecord.uid);
return {
message: 'Email successfully verified',
token: customToken,
};
} catch (error) {
throw new HttpException(error.message || 'Failed to verify email', HttpStatus.BAD_REQUEST);
}
}
@Post(':uid/role')
@UseGuards(AuthGuard, AdminGuard) // Only admins can change roles
async setUserRole(@Param('uid') uid: string, @Body('role') role: UserRole): Promise<{ success: boolean }> {
await this.authService.setUserRole(uid, role);
return { success: true };
}
@Get('me/role')
@UseGuards(AuthGuard)
async getMyRole(@Req() req: any): Promise<{ role: UserRole | null }> {
console.log('->', req.user);
console.log('-->', req.user.uid);
const uid = req.user.uid; // From FirebaseAuthGuard
const role = await this.authService.getUserRole(uid);
return { role };
}
@Get(':uid/role')
@UseGuards(AuthGuard)
async getUserRole(@Param('uid') uid: string): Promise<{ role: UserRole | null }> {
const role = await this.authService.getUserRole(uid);
return { role };
}
@Get('role/:role')
@UseGuards(AuthGuard, AdminGuard) // Only admins can list users by role
async getUsersByRole(@Param('role') role: UserRole): Promise<{ users: any[] }> {
const users = await this.authService.getUsersByRole(role);
// Map to simpler objects to avoid circular references
const simplifiedUsers = users.map(user => ({
uid: user.uid,
email: user.email,
displayName: user.displayName,
}));
return { users: simplifiedUsers };
}
/**
* Ruft alle Firebase-Benutzer mit ihren Rollen ab
* @param maxResults Maximale Anzahl an zurückzugebenden Benutzern (optional, Standard: 1000)
* @param pageToken Token für die Paginierung (optional)
* @returns Eine Liste von Benutzern mit ihren Rollen und Metadaten
*/
@Get() @Get()
@UseGuards(AuthGuard, AdminGuard) // Only admins can list all users getAccessToken(): any {
async getAllUsers(@Query('maxResults') maxResults?: number, @Query('pageToken') pageToken?: string): Promise<UsersResponse> { return this.authService.getAccessToken();
const result = await this.authService.getAllUsers(maxResults ? parseInt(maxResults.toString(), 10) : undefined, pageToken);
return {
users: result.users,
totalCount: result.users.length,
...(result.pageToken && { pageToken: result.pageToken }),
};
} }
/** @Get('users')
* Endpoint zum direkten Einstellen einer Rolle für Debug-Zwecke getUsers(): any {
* WARNUNG: Dieser Endpoint sollte in der Produktion entfernt oder stark gesichert werden return this.authService.getUsers();
*/
@Post('set-role')
@UseGuards(AuthGuard, LocalhostGuard)
async setUserRoleOnLocalhost(@Req() req: any, @Body('role') role: UserRole): Promise<{ success: boolean; message: string }> {
try {
const uid = req.user.uid;
// Aktuelle Rolle protokollieren
const currentUser = await this.authService.getUserRole(uid);
console.log(`Changing role for user ${uid} from ${currentUser} to ${role}`);
// Neue Rolle setzen
await this.authService.setUserRole(uid, role);
// Rolle erneut prüfen, um zu bestätigen
const newRole = await this.authService.getUserRole(uid);
return {
success: true,
message: `Rolle für Benutzer ${uid} von ${currentUser} zu ${newRole} geändert`,
};
} catch (error) {
console.error('Fehler beim Setzen der Rolle:', error);
return {
success: false,
message: `Fehler: ${error.message}`,
};
} }
@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

@ -1,14 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { PassportModule } from '@nestjs/passport';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module'; import { JwtStrategy } from '../jwt.strategy.js';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller.js';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service.js';
@Module({ @Module({
imports: [ConfigModule.forRoot({ envFilePath: '.env' }),FirebaseAdminModule], imports: [PassportModule],
providers: [AuthService, JwtStrategy],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService], exports: [AuthService],
exports: [],
}) })
export class AuthModule {} export class AuthModule {}

View File

@ -1,113 +1,119 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as admin from 'firebase-admin'; import ky from 'ky';
import { FirebaseUserInfo, UserRole } from 'src/models/main.model'; import urlcat from 'urlcat';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {} public async getAccessToken() {
const form = new FormData();
form.append('grant_type', 'password');
form.append('username', process.env.user);
form.append('password', process.env.password);
/**
* Set a user's role via Firebase custom claims
*/
async setUserRole(uid: string, role: UserRole): Promise<void> {
try { try {
// Get the current custom claims const params = new URLSearchParams();
const user = await this.firebaseAdmin.auth().getUser(uid); params.append('grant_type', 'password');
const currentClaims = user.customClaims || {}; params.append('username', process.env.user);
params.append('password', process.env.password);
const URL = `${process.env.host}${process.env.tokenURL}`;
// Set the new role const response = await ky
await this.firebaseAdmin.auth().setCustomUserClaims(uid, { .post(URL, {
...currentClaims, body: params.toString(),
role: role, headers: {
}); 'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3',
},
})
.json();
return (<any>response).access_token;
} catch (error) { } catch (error) {
console.error('Error setting user role:', error); if (error.name === 'HTTPError') {
throw error; const errorJson = await error.response.json();
console.error('Fehlerantwort vom Server:', errorJson);
} else {
console.error('Allgemeiner Fehler:', error);
}
} }
} }
/** public async getUsers() {
* Get a user's current role const token = await this.getAccessToken();
*/ const URL = `${process.env.host}${process.env.usersURL}`;
async getUserRole(uid: string): Promise<UserRole | null> { const response = await ky
try { .get(URL, {
const user = await this.firebaseAdmin.auth().getUser(uid); headers: {
const claims = user.customClaims || {}; 'Content-Type': 'application/x-www-form-urlencoded',
return (claims.role as UserRole) || null; Authorization: `Bearer ${token}`,
} catch (error) { },
console.error('Error getting user role:', error); })
throw error; .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) {
* Get all users with a specific role const token = await this.getAccessToken();
*/ const URL = urlcat(process.env.host, process.env.userGroupsURL, { userid });
async getUsersByRole(role: UserRole): Promise<admin.auth.UserRecord[]> { const response = await ky
// Note: Firebase Admin doesn't provide a direct way to query users by custom claims .get(URL, {
// For a production app, you might want to store role information in Firestore as well headers: {
// This is a simple implementation that lists all users and filters them 'Content-Type': 'application/x-www-form-urlencoded',
try { Authorization: `Bearer ${token}`,
const listUsersResult = await this.firebaseAdmin.auth().listUsers(); },
return listUsersResult.users.filter(user => user.customClaims && user.customClaims.role === role); })
} catch (error) { .json();
console.error('Error getting users by role:', error); return response;
throw error;
} }
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();
* Get all Firebase users with their roles const URL = urlcat(process.env.host, process.env.addUser2GroupURL, { userid, groupid });
* @param maxResults Maximum number of users to return (optional, default 1000) const response = await ky
* @param pageToken Token for pagination (optional) .put(URL, {
*/ headers: {
async getAllUsers(maxResults: number = 1000, pageToken?: string): Promise<{ users: FirebaseUserInfo[]; pageToken?: string }> { 'Content-Type': 'application/x-www-form-urlencoded',
try { Authorization: `Bearer ${token}`,
const listUsersResult = await this.firebaseAdmin.auth().listUsers(maxResults, pageToken); },
})
const users = listUsersResult.users.map(user => this.mapUserRecord(user)); .json();
return response;
return {
users,
pageToken: listUsersResult.pageToken,
};
} catch (error) {
console.error('Error getting all users:', error);
throw error;
}
}
/**
* Maps a Firebase UserRecord to our FirebaseUserInfo interface
*/
private mapUserRecord(user: admin.auth.UserRecord): FirebaseUserInfo {
return {
uid: user.uid,
email: user.email || null,
displayName: user.displayName || null,
photoURL: user.photoURL || null,
phoneNumber: user.phoneNumber || null,
disabled: user.disabled,
emailVerified: user.emailVerified,
role: user.customClaims?.role || null,
creationTime: user.metadata.creationTime,
lastSignInTime: user.metadata.lastSignInTime,
// Optionally include other customClaims if needed
customClaims: user.customClaims,
};
}
/**
* Set default role for a new user
*/
async setDefaultRole(uid: string): Promise<void> {
return this.setUserRole(uid, 'guest');
}
/**
* Verify if a user has a specific role
*/
async hasRole(uid: string, role: UserRole): Promise<boolean> {
const userRole = await this.getUserRole(uid);
return userRole === role;
} }
} }

View File

@ -1,8 +0,0 @@
// src/decorators/real-ip.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { getRealIpInfo, RealIpInfo } from '../utils/ip.util';
export const RealIp = createParamDecorator((data: unknown, ctx: ExecutionContext): RealIpInfo => {
const request = ctx.switchToHttp().getRequest();
return getRealIpInfo(request);
});

View File

@ -1,44 +1,24 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/node-postgres'; import { drizzle } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { ClsService } from 'nestjs-cls';
import pkg from 'pg'; import pkg from 'pg';
import { Logger } from 'winston';
import * as schema from './schema';
import { PG_CONNECTION } from './schema';
const { Pool } = pkg; 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({ @Module({
imports: [ConfigModule],
providers: [ providers: [
{ {
provide: PG_CONNECTION, provide: PG_CONNECTION,
inject: [ConfigService, WINSTON_MODULE_PROVIDER, ClsService], inject: [ConfigService],
useFactory: async (configService: ConfigService, logger: Logger, cls: ClsService) => { useFactory: async (configService: ConfigService) => {
const connectionString = configService.get<string>('DATABASE_URL'); const connectionString = configService.get<string>('DATABASE_URL');
// const dbHost = configService.get<string>('DB_HOST');
// const dbPort = configService.get<string>('DB_PORT');
// const dbName = configService.get<string>('DB_NAME');
// const dbUser = configService.get<string>('DB_USER');
const dbPassword = configService.get<string>('DB_PASSWORD');
// logger.info(`Drizzle Connection - URL: ${connectionString}, Host: ${dbHost}, Port: ${dbPort}, DB: ${dbName}, User: ${dbUser}`);
// console.log(`---> Drizzle Connection - URL: ${connectionString}, Host: ${dbHost}, Port: ${dbPort}, DB: ${dbName}, User: ${dbUser}`);
const pool = new Pool({ const pool = new Pool({
connectionString, connectionString,
// ssl: true, // Falls benötigt // ssl: true,
}); });
// Definiere einen benutzerdefinierten Logger für Drizzle return drizzle(pool, { schema, logger:true });
const drizzleLogger = {
logQuery(query: string, params: unknown[]): void {
const ip = cls.get('ip') || 'unknown';
const countryCode = cls.get('countryCode') || 'unknown';
const username = cls.get('username') || 'unknown';
logger.info(`IP: ${ip} (${countryCode}) (${username}) - Query: ${query} - Params: ${JSON.stringify(params)}`);
},
};
return drizzle(pool, { schema, logger: drizzleLogger });
}, },
}, },
], ],

View File

@ -1,34 +0,0 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { promises as fs } from 'fs';
import { Pool } from 'pg';
import * as schema from './schema';
// Drizzle-Tabellen-Definitionen (hier hast du bereits die Tabellen definiert, wir nehmen an, sie werden hier importiert)
import { businesses, commercials, users } from './schema'; // Anpassen je nach tatsächlicher Struktur
const connectionString = process.env.DATABASE_URL;
console.log(connectionString);
const client = new Pool({ connectionString });
const db = drizzle(client, { schema, logger: true });
(async () => {
try {
// Abfrage der Daten für jede Tabelle
const usersData = await db.select().from(users).execute();
const businessesData = await db.select().from(businesses).execute();
const commercialsData = await db.select().from(commercials).execute();
// Speichern der Daten in JSON-Dateien
await fs.writeFile('./data/users_export.json', JSON.stringify(usersData, null, 2));
console.log('Users exportiert in users.json');
await fs.writeFile('./data/businesses_export.json', JSON.stringify(businessesData, null, 2));
console.log('Businesses exportiert in businesses.json');
await fs.writeFile('./data/commercials_export.json', JSON.stringify(commercialsData, null, 2));
console.log('Commercials exportiert in commercials.json');
} catch (error) {
console.error('Fehler beim Exportieren der Tabellen:', error);
} finally {
await client.end();
}
})();

View File

@ -2,19 +2,20 @@ import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres'; import { drizzle } from 'drizzle-orm/node-postgres';
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs'; import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs';
import fs from 'fs-extra'; import fs from 'fs-extra';
import OpenAI from 'openai';
import { join } from 'path'; import { join } from 'path';
import { Pool } from 'pg'; import pkg from 'pg';
import { rimraf } from 'rimraf'; import { rimraf } from 'rimraf';
import sharp from 'sharp'; import sharp from 'sharp';
import { BusinessListingService } from 'src/listings/business-listing.service'; import { BusinessListingService } from 'src/listings/business-listing.service.js';
import { CommercialPropertyService } from 'src/listings/commercial-property.service'; import { CommercialPropertyService } from 'src/listings/commercial-property.service.js';
import { Geo } from 'src/models/server.model'; import { Geo } from 'src/models/server.model.js';
import { UserService } from 'src/user/user.service';
import winston from 'winston'; import winston from 'winston';
import { User, UserData } from '../models/db.model'; import { User, UserData } from '../models/db.model.js';
import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName } from '../models/main.model'; import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
import { SelectOptionsService } from '../select-options/select-options.service'; import { SelectOptionsService } from '../select-options/select-options.service.js';
import * as schema from './schema'; import { convertUserToDrizzleUser } from '../utils.js';
import * as schema from './schema.js';
interface PropertyImportListing { interface PropertyImportListing {
id: string; id: string;
userId: string; userId: string;
@ -53,69 +54,68 @@ interface BusinessImportListing {
internals: string; internals: string;
created: string; created: string;
} }
// const typesOfBusiness: Array<KeyValueStyle> = [ const typesOfBusiness: Array<KeyValueStyle> = [
// { name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' }, { name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
// { name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' }, { name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
// { name: 'Real Estate', value: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' }, { name: 'Real Estate', value: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
// { name: 'Uncategorized', value: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' }, { name: 'Uncategorized', value: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
// { name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' }, { name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' },
// { name: 'Oilfield SVE and MFG.', value: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' }, { name: 'Oilfield SVE and MFG.', value: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' },
// { name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' }, { name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
// { name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' }, { name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
// { name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' }, { name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' },
// { name: 'Franchise', value: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' }, { name: 'Franchise', value: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
// { name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' }, { name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' },
// { name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' }, { name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' },
// { name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' }, { name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
// ]; ];
// const { Pool } = pkg; const { Pool } = pkg;
// const openai = new OpenAI({ const openai = new OpenAI({
// apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen
// }); });
(async () => {
const connectionString = process.env.DATABASE_URL; const connectionString = process.env.DATABASE_URL;
// const pool = new Pool({connectionString}) // const pool = new Pool({connectionString})
const client = new Pool({ connectionString }); const client = new Pool({ connectionString });
const db = drizzle(client, { schema, logger: true }); const db = drizzle(client, { schema, logger: true });
const logger = winston.createLogger({ const logger = winston.createLogger({
transports: [new winston.transports.Console()], transports: [new winston.transports.Console()],
}); });
const commService = new CommercialPropertyService(null, db); const commService = new CommercialPropertyService(null, db);
const businessService = new BusinessListingService(null, db); const businessService = new BusinessListingService(null, db);
const userService = new UserService(null, db, null, null); //Delete Content
//Delete Content await db.delete(schema.commercials);
await db.delete(schema.commercials); await db.delete(schema.businesses);
await db.delete(schema.businesses); await db.delete(schema.users);
await db.delete(schema.users); let filePath = `./src/assets/geo.json`;
let filePath = `./src/assets/geo.json`; const rawData = readFileSync(filePath, 'utf8');
const rawData = readFileSync(filePath, 'utf8'); const geos = JSON.parse(rawData) as Geo;
const geos = JSON.parse(rawData) as Geo;
const sso = new SelectOptionsService(); const sso = new SelectOptionsService();
//Broker //Broker
filePath = `./data/broker.json`; filePath = `./data/broker.json`;
let data: string = readFileSync(filePath, 'utf8'); let data: string = readFileSync(filePath, 'utf8');
const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten
const generatedUserData = []; const generatedUserData = [];
console.log(usersData.length); console.log(usersData.length);
let i = 0, let i = 0,
male = 0, male = 0,
female = 0; female = 0;
const targetPathProfile = `./pictures/profile`; const targetPathProfile = `./pictures/profile`;
deleteFilesOfDir(targetPathProfile); deleteFilesOfDir(targetPathProfile);
const targetPathLogo = `./pictures/logo`; const targetPathLogo = `./pictures/logo`;
deleteFilesOfDir(targetPathLogo); deleteFilesOfDir(targetPathLogo);
const targetPathProperty = `./pictures/property`; const targetPathProperty = `./pictures/property`;
deleteFilesOfDir(targetPathProperty); deleteFilesOfDir(targetPathProperty);
fs.ensureDirSync(`./pictures/logo`); fs.ensureDirSync(`./pictures/logo`);
fs.ensureDirSync(`./pictures/profile`); fs.ensureDirSync(`./pictures/profile`);
fs.ensureDirSync(`./pictures/property`); fs.ensureDirSync(`./pictures/property`);
//User //User
for (let index = 0; index < usersData.length; index++) { for (let index = 0; index < usersData.length; index++) {
const userData = usersData[index]; const userData = usersData[index];
const user: User = createDefaultUser('', '', '', null); const user: User = createDefaultUser('', '', '');
user.licensedIn = []; user.licensedIn = [];
userData.licensedIn.forEach(l => { userData.licensedIn.forEach(l => {
console.log(l['value'], l['name']); console.log(l['value'], l['name']);
@ -136,12 +136,12 @@ interface BusinessImportListing {
user.companyOverview = userData.companyOverview; user.companyOverview = userData.companyOverview;
user.companyWebsite = userData.companyWebsite; user.companyWebsite = userData.companyWebsite;
const [city, state] = userData.companyLocation.split('-').map(e => e.trim()); const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
user.location = {}; user.companyLocation = {};
user.location.name = city; user.companyLocation.city = city;
user.location.state = state; user.companyLocation.state = state;
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city); const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
user.location.latitude = cityGeo.latitude; user.companyLocation.latitude = cityGeo.latitude;
user.location.longitude = cityGeo.longitude; user.companyLocation.longitude = cityGeo.longitude;
user.offeredServices = userData.offeredServices; user.offeredServices = userData.offeredServices;
user.gender = userData.gender; user.gender = userData.gender;
user.customerType = 'professional'; user.customerType = 'professional';
@ -149,32 +149,31 @@ interface BusinessImportListing {
user.created = new Date(); user.created = new Date();
user.updated = new Date(); user.updated = new Date();
// const u = await db const u = await db
// .insert(schema.users) .insert(schema.users)
// .values(convertUserToDrizzleUser(user)) .values(convertUserToDrizzleUser(user))
// .returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname }); .returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname });
const u = await userService.saveUser(user); generatedUserData.push(u[0]);
generatedUserData.push(u);
i++; i++;
logger.info(`user_${index} inserted`); logger.info(`user_${index} inserted`);
if (u.gender === 'male') { if (u[0].gender === 'male') {
male++; male++;
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`); const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
await storeProfilePicture(data, emailToDirName(u.email)); await storeProfilePicture(data, emailToDirName(u[0].email));
} else { } else {
female++; female++;
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`); const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
await storeProfilePicture(data, emailToDirName(u.email)); await storeProfilePicture(data, emailToDirName(u[0].email));
} }
const data = readFileSync(`./pictures_base/logo/${i}.jpg`); const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
await storeCompanyLogo(data, emailToDirName(u.email)); await storeCompanyLogo(data, emailToDirName(u[0].email));
} }
//Corporate Listings //Corporate Listings
filePath = `./data/commercials.json`; filePath = `./data/commercials.json`;
data = readFileSync(filePath, 'utf8'); data = readFileSync(filePath, 'utf8');
const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten
for (let index = 0; index < commercialJsonData.length; index++) { for (let index = 0; index < commercialJsonData.length; index++) {
const user = getRandomItem(generatedUserData); const user = getRandomItem(generatedUserData);
const commercial = createDefaultCommercialPropertyListing(); const commercial = createDefaultCommercialPropertyListing();
const id = commercialJsonData[index].id; const id = commercialJsonData[index].id;
@ -189,7 +188,7 @@ interface BusinessImportListing {
commercial.location = {}; commercial.location = {};
commercial.location.latitude = cityGeo.latitude; commercial.location.latitude = cityGeo.latitude;
commercial.location.longitude = cityGeo.longitude; commercial.location.longitude = cityGeo.longitude;
commercial.location.name = commercialJsonData[index].city; commercial.location.city = commercialJsonData[index].city;
commercial.location.state = commercialJsonData[index].state; commercial.location.state = commercialJsonData[index].state;
// console.log(JSON.stringify(commercial.location)); // console.log(JSON.stringify(commercial.location));
} catch (e) { } catch (e) {
@ -211,13 +210,13 @@ interface BusinessImportListing {
} catch (err) { } catch (err) {
console.log(`----- No pictures available for ${id} ------ ${err}`); console.log(`----- No pictures available for ${id} ------ ${err}`);
} }
} }
//Business Listings //Business Listings
filePath = `./data/businesses.json`; filePath = `./data/businesses.json`;
data = readFileSync(filePath, 'utf8'); data = readFileSync(filePath, 'utf8');
const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten
for (let index = 0; index < businessJsonData.length; index++) { for (let index = 0; index < businessJsonData.length; index++) {
const business = createDefaultBusinessListing(); //businessJsonData[index]; const business = createDefaultBusinessListing(); //businessJsonData[index];
delete business.id; delete business.id;
const user = getRandomItem(generatedUserData); const user = getRandomItem(generatedUserData);
@ -230,7 +229,7 @@ interface BusinessImportListing {
business.location = {}; business.location = {};
business.location.latitude = cityGeo.latitude; business.location.latitude = cityGeo.latitude;
business.location.longitude = cityGeo.longitude; business.location.longitude = cityGeo.longitude;
business.location.name = businessJsonData[index].city; business.location.city = businessJsonData[index].city;
business.location.state = businessJsonData[index].state; business.location.state = businessJsonData[index].state;
} catch (e) { } catch (e) {
console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`); console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`);
@ -258,21 +257,21 @@ interface BusinessImportListing {
business.updated = new Date(businessJsonData[index].created); business.updated = new Date(businessJsonData[index].created);
await businessService.createListing(business); //db.insert(schema.businesses).values(business); await businessService.createListing(business); //db.insert(schema.businesses).values(business);
} }
//End //End
await client.end(); await client.end();
})();
// function sleep(ms) { function sleep(ms) {
// return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
// } }
// async function createEmbedding(text: string): Promise<number[]> { async function createEmbedding(text: string): Promise<number[]> {
// const response = await openai.embeddings.create({ const response = await openai.embeddings.create({
// model: 'text-embedding-3-small', model: 'text-embedding-3-small',
// input: text, input: text,
// }); });
// return response.data[0].embedding; return response.data[0].embedding;
// } }
function getRandomItem<T>(arr: T[]): T { function getRandomItem<T>(arr: T[]): T {
if (arr.length === 0) { if (arr.length === 0) {
@ -284,7 +283,7 @@ function getRandomItem<T>(arr: T[]): T {
} }
function getFilenames(id: string): string[] { function getFilenames(id: string): string[] {
try { try {
const filePath = `./pictures_base/property/${id}`; let filePath = `./pictures_base/property/${id}`;
return readdirSync(filePath); return readdirSync(filePath);
} catch (e) { } catch (e) {
return []; return [];
@ -301,7 +300,7 @@ function getRandomDateWithinLastYear(): Date {
return randomDate; return randomDate;
} }
async function storeProfilePicture(buffer: Buffer, userId: string) { async function storeProfilePicture(buffer: Buffer, userId: string) {
const quality = 50; let quality = 50;
const output = await sharp(buffer) const output = await sharp(buffer)
.resize({ width: 300 }) .resize({ width: 300 })
.avif({ quality }) // Verwende AVIF .avif({ quality }) // Verwende AVIF
@ -311,7 +310,7 @@ async function storeProfilePicture(buffer: Buffer, userId: string) {
} }
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) { async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
const quality = 50; let quality = 50;
const output = await sharp(buffer) const output = await sharp(buffer)
.resize({ width: 300 }) .resize({ width: 300 })
.avif({ quality }) // Verwende AVIF .avif({ quality }) // Verwende AVIF

View File

@ -1,68 +0,0 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { readFileSync } from 'fs';
import { Pool } from 'pg';
import { BusinessListingService } from 'src/listings/business-listing.service';
import { CommercialPropertyService } from 'src/listings/commercial-property.service';
import { UserService } from 'src/user/user.service';
import winston from 'winston';
import { BusinessListing, CommercialPropertyListing, User } from '../models/db.model';
import * as schema from './schema';
(async () => {
const connectionString = process.env.DATABASE_URL;
const client = new Pool({ connectionString });
const db = drizzle(client, { schema, logger: true });
const logger = winston.createLogger({
transports: [new winston.transports.Console()],
});
const commService = new CommercialPropertyService(null, db);
const businessService = new BusinessListingService(null, db);
const userService = new UserService(null, db, null, null);
//Delete Content
await db.delete(schema.commercials);
await db.delete(schema.businesses);
await db.delete(schema.users);
let filePath = `./data/users_export.json`;
let data: string = readFileSync(filePath, 'utf8');
const usersData: User[] = JSON.parse(data); // Erwartet ein Array von Objekten
for (let index = 0; index < usersData.length; index++) {
const user = usersData[index];
delete user.id;
const u = await userService.saveUser(user, false);
logger.info(`user_${index} inserted`);
}
//Corporate Listings
filePath = `./data/commercials_export.json`;
data = readFileSync(filePath, 'utf8');
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
for (let index = 0; index < commercialJsonData.length; index++) {
const commercial = commercialJsonData[index];
delete commercial.id;
const result = await commService.createListing(commercial);
}
//Business Listings
filePath = `./data/businesses_export.json`;
data = readFileSync(filePath, 'utf8');
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
for (let index = 0; index < businessJsonData.length; index++) {
const business = businessJsonData[index];
delete business.id;
await businessService.createListing(business);
}
//End
await client.end();
})();
function getRandomItem<T>(arr: T[]): T {
if (arr.length === 0) {
throw new Error('The array is empty.');
}
const randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex];
}

View File

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

View File

@ -0,0 +1,114 @@
DO $$ BEGIN
CREATE TYPE "public"."customerSubType" AS ENUM('broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
CREATE TYPE "public"."customerType" AS ENUM('buyer', 'professional');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
CREATE TYPE "public"."gender" AS ENUM('male', 'female');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
CREATE TYPE "public"."listingsCategory" AS ENUM('commercialProperty', 'business');
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,
"email" varchar(255),
"type" varchar(255),
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"draft" boolean,
"listingsCategory" "listingsCategory",
"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,
"imageName" varchar(200),
"created" timestamp,
"updated" timestamp,
"latitude" double precision,
"longitude" double precision
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"serialId" serial NOT NULL,
"email" varchar(255),
"type" varchar(255),
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"listingsCategory" "listingsCategory",
"draft" boolean,
"imageOrder" varchar(200)[],
"imagePath" varchar(200),
"created" timestamp,
"updated" timestamp,
"latitude" double precision,
"longitude" double precision
);
--> 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),
"city" varchar(255),
"state" char(2),
"offeredServices" text,
"areasServed" jsonb,
"hasProfile" boolean,
"hasCompanyLogo" boolean,
"licensedIn" jsonb,
"gender" "gender",
"customerType" "customerType",
"customerSubType" "customerSubType",
"created" timestamp,
"updated" timestamp,
"latitude" double precision,
"longitude" double precision,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "businesses" ADD CONSTRAINT "businesses_email_users_email_fk" FOREIGN KEY ("email") REFERENCES "public"."users"("email") 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_email_users_email_fk" FOREIGN KEY ("email") REFERENCES "public"."users"("email") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@ -0,0 +1,541 @@
{
"id": "a8283ca6-2c10-42bb-a640-ca984544ba30",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"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": "listingsCategory",
"typeSchema": "public",
"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
},
"imageName": {
"name": "imageName",
"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
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_email_users_email_fk": {
"name": "businesses_email_users_email_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"serialId": {
"name": "serialId",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"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": "listingsCategory",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"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
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_email_users_email_fk": {
"name": "commercials_email_users_email_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.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
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"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",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerType": {
"name": "customerType",
"type": "customerType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerSubType": {
"name": "customerSubType",
"type": "customerSubType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
}
},
"enums": {
"public.customerSubType": {
"name": "customerSubType",
"schema": "public",
"values": [
"broker",
"cpa",
"attorney",
"titleCompany",
"surveyor",
"appraiser"
]
},
"public.customerType": {
"name": "customerType",
"schema": "public",
"values": [
"buyer",
"professional"
]
},
"public.gender": {
"name": "gender",
"schema": "public",
"values": [
"male",
"female"
]
},
"public.listingsCategory": {
"name": "listingsCategory",
"schema": "public",
"values": [
"commercialProperty",
"business"
]
}
},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1723045357281,
"tag": "0000_lean_marvex",
"breakpoints": true
}
]
}

View File

@ -1,66 +1,12 @@
import { sql } from 'drizzle-orm'; import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
import { boolean, doublePrecision, index, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
import { AreasServed, LicensedIn } from '../models/db.model'; import { AreasServed, LicensedIn } from '../models/db.model';
export const PG_CONNECTION = 'PG_CONNECTION'; export const PG_CONNECTION = 'PG_CONNECTION';
export const genderEnum = pgEnum('gender', ['male', 'female']); export const genderEnum = pgEnum('gender', ['male', 'female']);
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'seller', 'professional']); export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']);
export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']); export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']); export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
export const subscriptionTypeEnum = pgEnum('subscriptionType', ['free', 'professional', 'broker']);
// Neue JSONB-basierte Tabellen export const users = pgTable('users', {
export const users_json = pgTable(
'users_json',
{
id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
data: jsonb('data'),
},
table => ({
emailIdx: index('idx_users_json_email').on(table.email),
}),
);
export const businesses_json = pgTable(
'businesses_json',
{
id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }).references(() => users_json.email),
data: jsonb('data'),
},
table => ({
emailIdx: index('idx_businesses_json_email').on(table.email),
}),
);
export const commercials_json = pgTable(
'commercials_json',
{
id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }).references(() => users_json.email),
data: jsonb('data'),
},
table => ({
emailIdx: index('idx_commercials_json_email').on(table.email),
}),
);
export const listing_events_json = pgTable(
'listing_events_json',
{
id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }),
data: jsonb('data'),
},
table => ({
emailIdx: index('idx_listing_events_json_email').on(table.email),
}),
);
// Bestehende Tabellen bleiben unverändert
export const users = pgTable(
'users',
{
id: uuid('id').primaryKey().defaultRandom().notNull(), id: uuid('id').primaryKey().defaultRandom().notNull(),
firstname: varchar('firstname', { length: 255 }).notNull(), firstname: varchar('firstname', { length: 255 }).notNull(),
lastname: varchar('lastname', { length: 255 }).notNull(), lastname: varchar('lastname', { length: 255 }).notNull(),
@ -70,6 +16,8 @@ export const users = pgTable(
companyName: varchar('companyName', { length: 255 }), companyName: varchar('companyName', { length: 255 }),
companyOverview: text('companyOverview'), companyOverview: text('companyOverview'),
companyWebsite: varchar('companyWebsite', { length: 255 }), companyWebsite: varchar('companyWebsite', { length: 255 }),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
offeredServices: text('offeredServices'), offeredServices: text('offeredServices'),
areasServed: jsonb('areasServed').$type<AreasServed[]>(), areasServed: jsonb('areasServed').$type<AreasServed[]>(),
hasProfile: boolean('hasProfile'), hasProfile: boolean('hasProfile'),
@ -80,30 +28,25 @@ export const users = pgTable(
customerSubType: customerSubTypeEnum('customerSubType'), customerSubType: customerSubTypeEnum('customerSubType'),
created: timestamp('created'), created: timestamp('created'),
updated: timestamp('updated'), updated: timestamp('updated'),
subscriptionId: text('subscriptionId'), latitude: doublePrecision('latitude'),
subscriptionPlan: subscriptionTypeEnum('subscriptionPlan'), longitude: doublePrecision('longitude'),
location: jsonb('location'), // embedding: vector('embedding', { dimensions: 1536 }),
showInDirectory: boolean('showInDirectory').default(true), });
},
table => ({
locationUserCityStateIdx: index('idx_user_location_city_state').on(
sql`((${table.location}->>'name')::varchar), ((${table.location}->>'state')::varchar), ((${table.location}->>'latitude')::float), ((${table.location}->>'longitude')::float)`,
),
}),
);
export const businesses = pgTable( export const businesses = pgTable('businesses', {
'businesses',
{
id: uuid('id').primaryKey().defaultRandom().notNull(), id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }).references(() => users.email), email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }), type: varchar('type', { length: 255 }),
title: varchar('title', { length: 255 }), title: varchar('title', { length: 255 }),
description: text('description'), description: text('description'),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
// zipCode: integer('zipCode'),
// county: varchar('county', { length: 255 }),
price: doublePrecision('price'), price: doublePrecision('price'),
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
draft: boolean('draft'), draft: boolean('draft'),
listingsCategory: listingsCategoryEnum('listingsCategory'), listingsCategory: listingsCategoryEnum('listingsCategory'), //varchar('listingsCategory', { length: 255 }),
realEstateIncluded: boolean('realEstateIncluded'), realEstateIncluded: boolean('realEstateIncluded'),
leasedLocation: boolean('leasedLocation'), leasedLocation: boolean('leasedLocation'),
franchiseResale: boolean('franchiseResale'), franchiseResale: boolean('franchiseResale'),
@ -119,53 +62,31 @@ export const businesses = pgTable(
imageName: varchar('imageName', { length: 200 }), imageName: varchar('imageName', { length: 200 }),
created: timestamp('created'), created: timestamp('created'),
updated: timestamp('updated'), updated: timestamp('updated'),
location: jsonb('location'), latitude: doublePrecision('latitude'),
}, longitude: doublePrecision('longitude'),
table => ({ // embedding: vector('embedding', { dimensions: 1536 }),
locationBusinessCityStateIdx: index('idx_business_location_city_state').on( });
sql`((${table.location}->>'name')::varchar), ((${table.location}->>'state')::varchar), ((${table.location}->>'latitude')::float), ((${table.location}->>'longitude')::float)`,
),
}),
);
export const commercials = pgTable( export const commercials = pgTable('commercials', {
'commercials',
{
id: uuid('id').primaryKey().defaultRandom().notNull(), id: uuid('id').primaryKey().defaultRandom().notNull(),
serialId: serial('serialId'), serialId: serial('serialId'),
email: varchar('email', { length: 255 }).references(() => users.email), email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }), type: varchar('type', { length: 255 }),
title: varchar('title', { length: 255 }), title: varchar('title', { length: 255 }),
description: text('description'), description: text('description'),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
price: doublePrecision('price'), price: doublePrecision('price'),
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
listingsCategory: listingsCategoryEnum('listingsCategory'), listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }),
draft: boolean('draft'), draft: boolean('draft'),
// zipCode: integer('zipCode'),
// county: varchar('county', { length: 255 }),
imageOrder: varchar('imageOrder', { length: 200 }).array(), imageOrder: varchar('imageOrder', { length: 200 }).array(),
imagePath: varchar('imagePath', { length: 200 }), imagePath: varchar('imagePath', { length: 200 }),
created: timestamp('created'), created: timestamp('created'),
updated: timestamp('updated'), updated: timestamp('updated'),
location: jsonb('location'), latitude: doublePrecision('latitude'),
}, longitude: doublePrecision('longitude'),
table => ({ // embedding: vector('embedding', { dimensions: 1536 }),
locationCommercialsCityStateIdx: index('idx_commercials_location_city_state').on(
sql`((${table.location}->>'name')::varchar), ((${table.location}->>'state')::varchar), ((${table.location}->>'latitude')::float), ((${table.location}->>'longitude')::float)`,
),
}),
);
export const listing_events = pgTable('listing_events', {
id: uuid('id').primaryKey().defaultRandom().notNull(),
listingId: varchar('listing_id', { length: 255 }),
email: varchar('email', { length: 255 }),
eventType: varchar('event_type', { length: 50 }),
eventTimestamp: timestamp('event_timestamp').defaultNow(),
userIp: varchar('user_ip', { length: 45 }),
userAgent: varchar('user_agent', { length: 255 }),
locationCountry: varchar('location_country', { length: 100 }),
locationCity: varchar('location_city', { length: 100 }),
locationLat: varchar('location_lat', { length: 20 }),
locationLng: varchar('location_lng', { length: 20 }),
referrer: varchar('referrer', { length: 255 }),
additionalData: jsonb('additional_data'),
}); });

View File

@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
// Angenommen, du hast eine Datei `databaseModels.js` mit deinen pgTable-Definitionen // Angenommen, du hast eine Datei `databaseModels.js` mit deinen pgTable-Definitionen
const { users } = require('./schema'); const { users } = require('./schema.js');
function generateTypeScriptInterface(tableDefinition, tableName) { function generateTypeScriptInterface(tableDefinition, tableName) {
let interfaceString = `export interface ${tableName} {\n`; let interfaceString = `export interface ${tableName} {\n`;

View File

@ -1,24 +0,0 @@
import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common';
import { RealIp } from 'src/decorators/real-ip.decorator';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { ListingEvent } from 'src/models/db.model';
import { RealIpInfo } from 'src/models/main.model';
import { EventService } from './event.service';
@Controller('event')
export class EventController {
constructor(private eventService: EventService) {}
@UseGuards(OptionalAuthGuard)
@Post()
async createEvent(
@Body() event: ListingEvent, // Struktur des Body-Objekts entsprechend anpassen
@RealIp() ipInfo: RealIpInfo, // IP Adresse des Clients
@Headers('user-agent') userAgent: string, // User-Agent des Clients
) {
event.userIp = ipInfo.ip;
event.userAgent = userAgent;
await this.eventService.createEvent(event);
return { message: 'Event gespeichert' };
}
}

View File

@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { DrizzleModule } from 'src/drizzle/drizzle.module';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
import { EventController } from './event.controller';
import { EventService } from './event.service';
@Module({
imports: [DrizzleModule,FirebaseAdminModule],
controllers: [EventController],
providers: [EventService],
})
export class EventModule {}

View File

@ -1,23 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { ListingEvent } from 'src/models/db.model';
import { Logger } from 'winston';
import * as schema from '../drizzle/schema';
import { listing_events_json, PG_CONNECTION } from '../drizzle/schema';
@Injectable()
export class EventService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
) {}
async createEvent(event: ListingEvent) {
// Speichere das Event in der Datenbank
event.eventTimestamp = new Date();
const { id, email, ...rest } = event;
const convertedEvent = { email, data: rest };
await this.conn.insert(listing_events_json).values(convertedEvent).execute();
}
}

View File

@ -1,22 +1,41 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { readFileSync } from 'fs';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import path, { join } from 'path';
import sharp from 'sharp'; import sharp from 'sharp';
import { fileURLToPath } from 'url';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ImageProperty, Subscription } from '../models/main.model.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@Injectable() @Injectable()
export class FileService { export class FileService {
private subscriptions: any;
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
this.loadSubscriptions();
fs.ensureDirSync(`./pictures`); fs.ensureDirSync(`./pictures`);
fs.ensureDirSync(`./pictures/profile`); fs.ensureDirSync(`./pictures/profile`);
fs.ensureDirSync(`./pictures/logo`); fs.ensureDirSync(`./pictures/logo`);
fs.ensureDirSync(`./pictures/property`); fs.ensureDirSync(`./pictures/property`);
} }
// ############ // ############
// Subscriptions
// ############
private loadSubscriptions(): void {
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
const rawData = readFileSync(filePath, 'utf8');
this.subscriptions = JSON.parse(rawData);
}
getSubscriptions(): Subscription[] {
return this.subscriptions;
}
// ############
// Profile // Profile
// ############ // ############
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) { async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
const quality = 50; let quality = 50;
const output = await sharp(file.buffer) const output = await sharp(file.buffer)
.resize({ width: 300 }) .resize({ width: 300 })
.avif({ quality }) // Verwende AVIF .avif({ quality }) // Verwende AVIF
@ -31,7 +50,7 @@ export class FileService {
// Logo // Logo
// ############ // ############
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) { async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
const quality = 50; let quality = 50;
const output = await sharp(file.buffer) const output = await sharp(file.buffer)
.resize({ width: 300 }) .resize({ width: 300 })
.avif({ quality }) // Verwende AVIF .avif({ quality }) // Verwende AVIF
@ -60,6 +79,7 @@ export class FileService {
} }
} }
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> { async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
const result: ImageProperty[] = [];
const directory = `./pictures/property/${imagePath}/${serial}`; const directory = `./pictures/property/${imagePath}/${serial}`;
if (fs.existsSync(directory)) { if (fs.existsSync(directory)) {
const files = await fs.readdir(directory); const files = await fs.readdir(directory);
@ -69,6 +89,7 @@ export class FileService {
} }
} }
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> { 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}`; const directory = `./pictures/property/${imagePath}/${serial}`;
fs.ensureDirSync(`${directory}`); fs.ensureDirSync(`${directory}`);
const imageName = await this.getNextImageName(directory); const imageName = await this.getNextImageName(directory);
@ -95,15 +116,16 @@ export class FileService {
} }
} }
async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) { async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) {
const quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
const start = Date.now(); let output;
const output = await sharp(buffer) let start = Date.now();
output = await sharp(buffer)
.resize({ width: 1500 }) .resize({ width: 1500 })
.avif({ quality }) // Verwende AVIF .avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp //.webp({ quality }) // Verwende Webp
.toBuffer(); .toBuffer();
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
const timeTaken = Date.now() - start; let timeTaken = Date.now() - start;
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`); this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
} }
deleteImage(path: string) { deleteImage(path: string) {

View File

@ -1,30 +0,0 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as admin from 'firebase-admin';
@Module({
imports: [ConfigModule],
providers: [
{
provide: 'FIREBASE_ADMIN',
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const serviceAccount = {
projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
clientEmail: configService.get<string>('FIREBASE_CLIENT_EMAIL'),
privateKey: configService.get<string>('FIREBASE_PRIVATE_KEY')?.replace(/\\n/g, '\n'),
};
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}
return admin;
},
},
],
exports: ['FIREBASE_ADMIN'],
})
export class FirebaseAdminModule {}

View File

@ -1,41 +1,27 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { RealIp } from 'src/decorators/real-ip.decorator'; import { CountyRequest } from 'src/models/server.model.js';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard'; import { GeoService } from './geo.service.js';
import { RealIpInfo } from 'src/models/main.model';
import { CountyRequest } from 'src/models/server.model';
import { GeoService } from './geo.service';
@Controller('geo') @Controller('geo')
export class GeoController { export class GeoController {
constructor(private geoService: GeoService) {} constructor(private geoService: GeoService) {}
@UseGuards(OptionalAuthGuard)
@Get(':prefix') @Get(':prefix')
findByPrefix(@Param('prefix') prefix: string): any { findByPrefix(@Param('prefix') prefix: string): any {
return this.geoService.findCitiesStartingWith(prefix); return this.geoService.findCitiesStartingWith(prefix);
} }
@UseGuards(OptionalAuthGuard)
@Get('citiesandstates/:prefix') @Get('citiesandstates/:prefix')
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any { findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
return this.geoService.findCitiesAndStatesStartingWith(prefix); return this.geoService.findCitiesAndStatesStartingWith(prefix);
} }
@UseGuards(OptionalAuthGuard)
@Get(':prefix/:state') @Get(':prefix/:state')
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any { findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
return this.geoService.findCitiesStartingWith(prefix, state); return this.geoService.findCitiesStartingWith(prefix, state);
} }
@UseGuards(OptionalAuthGuard)
@Post('counties') @Post('counties')
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any { findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states); return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states);
} }
@UseGuards(OptionalAuthGuard)
@Get('ipinfo/georesult/wysiwyg')
async fetchIpAndGeoLocation(@RealIp() ipInfo: RealIpInfo): Promise<any> {
return await this.geoService.fetchIpAndGeoLocation(ipInfo);
}
} }

View File

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

View File

@ -1,18 +1,18 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import path, { join } from 'path';
import { join } from 'path'; import { CountyResult, GeoResult } from 'src/models/main.model.js';
import { CityAndStateResult, CountyResult, GeoResult, IpInfo, RealIpInfo } from 'src/models/main.model'; import { fileURLToPath } from 'url';
import { Logger } from 'winston'; import { City, CountyData, Geo, State } from '../models/server.model.js';
import { City, CountyData, Geo, State } from '../models/server.model';
// 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 GeoService { export class GeoService {
geo: Geo; geo: Geo;
counties: CountyData[]; counties: CountyData[];
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { constructor() {
this.loadGeo(); this.loadGeo();
} }
private loadGeo(): void { private loadGeo(): void {
@ -24,13 +24,13 @@ export class GeoService {
this.counties = JSON.parse(rawCountiesData); this.counties = JSON.parse(rawCountiesData);
} }
findCountiesStartingWith(prefix: string, states?: string[]) { findCountiesStartingWith(prefix: string, states?: string[]) {
const results: CountyResult[] = []; let results: CountyResult[] = [];
let idCounter = 1; let idCounter = 1;
this.counties.forEach(stateData => { this.counties.forEach(stateData => {
if (!states || states.includes(stateData.state)) { if (!states || states.includes(stateData.state)) {
stateData.counties.forEach(county => { stateData.counties.forEach(county => {
if (county.startsWith(prefix?.toUpperCase())) { if (county.startsWith(prefix.toUpperCase())) {
results.push({ results.push({
id: idCounter++, id: idCounter++,
name: county, name: county,
@ -52,7 +52,7 @@ export class GeoService {
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) { if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
result.push({ result.push({
id: city.id, id: city.id,
name: city.name, city: city.name,
state: state.state_code, state: state.state_code,
//state_code: state.state_code, //state_code: state.state_code,
latitude: city.latitude, latitude: city.latitude,
@ -63,8 +63,8 @@ export class GeoService {
}); });
return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result; return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result;
} }
findCitiesAndStatesStartingWith(prefix: string): Array<CityAndStateResult> { findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> {
const results: Array<CityAndStateResult> = []; const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
const lowercasePrefix = prefix.toLowerCase(); const lowercasePrefix = prefix.toLowerCase();
@ -73,9 +73,10 @@ export class GeoService {
for (const state of this.geo.states) { for (const state of this.geo.states) {
if (state.name.toLowerCase().startsWith(lowercasePrefix)) { if (state.name.toLowerCase().startsWith(lowercasePrefix)) {
results.push({ results.push({
id: state.id, id: state.id.toString(),
name: state.name,
type: 'state', type: 'state',
content: state, state: state.state_code,
}); });
} }
@ -83,9 +84,10 @@ export class GeoService {
for (const city of state.cities) { for (const city of state.cities) {
if (city.name.toLowerCase().startsWith(lowercasePrefix)) { if (city.name.toLowerCase().startsWith(lowercasePrefix)) {
results.push({ results.push({
id: city.id, id: city.id.toString(),
name: city.name,
type: 'city', type: 'city',
content: { state: state.state_code, ...city }, state: state.state_code,
}); });
} }
} }
@ -95,27 +97,10 @@ export class GeoService {
return results.sort((a, b) => { return results.sort((a, b) => {
if (a.type === 'state' && b.type === 'city') return -1; if (a.type === 'state' && b.type === 'city') return -1;
if (a.type === 'city' && b.type === 'state') return 1; if (a.type === 'city' && b.type === 'state') return 1;
return a.content.name.localeCompare(b.content.name); return a.name.localeCompare(b.name);
}); });
} }
getCityWithCoords(state: string, city: string): City { getCityWithCoords(state: string, city: string): City {
return this.geo.states.find(s => s.state_code === state).cities.find(c => c.name === city); return this.geo.states.find(s => s.state_code === state).cities.find(c => c.name === city);
} }
async fetchIpAndGeoLocation(ipInfo: RealIpInfo): Promise<IpInfo> {
this.logger.info(`IP:${ipInfo.ip} - CountryCode:${ipInfo.countryCode}`);
const response = await fetch(`${process.env.IP_INFO_URL}/${ipInfo.ip}/geo?token=${process.env.IP_INFO_TOKEN}`, {
method: 'GET',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Fügen Sie den Ländercode aus Cloudflare hinzu, falls verfügbar
if (ipInfo.countryCode) {
data.cloudflareCountry = ipInfo.countryCode;
}
return data;
}
} }

View File

@ -1,11 +1,10 @@
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { Controller, Delete, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service.js';
import { CommercialPropertyService } from '../listings/commercial-property.service'; import { CommercialPropertyService } from '../listings/commercial-property.service.js';
import { SelectOptionsService } from '../select-options/select-options.service'; import { SelectOptionsService } from '../select-options/select-options.service.js';
@Controller('image') @Controller('image')
export class ImageController { export class ImageController {
@ -18,14 +17,12 @@ export class ImageController {
// ############ // ############
// Property // Property
// ############ // ############
@UseGuards(AuthGuard)
@Post('uploadPropertyPicture/:imagePath/:serial') @Post('uploadPropertyPicture/:imagePath/:serial')
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) { async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial); const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
await this.listingService.addImage(imagePath, serial, imagename); await this.listingService.addImage(imagePath, serial, imagename);
} }
@UseGuards(AuthGuard)
@Delete('propertyPicture/:imagePath/:serial/:imagename') @Delete('propertyPicture/:imagePath/:serial/:imagename')
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> { async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`); this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
@ -34,13 +31,11 @@ export class ImageController {
// ############ // ############
// Profile // Profile
// ############ // ############
@UseGuards(AuthGuard)
@Post('uploadProfile/:email') @Post('uploadProfile/:email')
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) { async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
await this.fileService.storeProfilePicture(file, adjustedEmail); await this.fileService.storeProfilePicture(file, adjustedEmail);
} }
@UseGuards(AuthGuard)
@Delete('profile/:email/') @Delete('profile/:email/')
async deleteProfileImagesById(@Param('email') email: string): Promise<any> { async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
this.fileService.deleteImage(`pictures/profile/${email}.avif`); this.fileService.deleteImage(`pictures/profile/${email}.avif`);
@ -48,13 +43,11 @@ export class ImageController {
// ############ // ############
// Logo // Logo
// ############ // ############
@UseGuards(AuthGuard)
@Post('uploadCompanyLogo/:email') @Post('uploadCompanyLogo/:email')
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) { async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
await this.fileService.storeCompanyLogo(file, adjustedEmail); await this.fileService.storeCompanyLogo(file, adjustedEmail);
} }
@UseGuards(AuthGuard)
@Delete('logo/:email/') @Delete('logo/:email/')
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> { async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`); this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);

View File

@ -1,13 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module'; import { FileService } from '../file/file.service.js';
import { FileService } from '../file/file.service'; import { ListingsModule } from '../listings/listings.module.js';
import { ListingsModule } from '../listings/listings.module'; import { SelectOptionsService } from '../select-options/select-options.service.js';
import { SelectOptionsService } from '../select-options/select-options.service'; import { ImageController } from './image.controller.js';
import { ImageController } from './image.controller'; import { ImageService } from './image.service.js';
import { ImageService } from './image.service';
@Module({ @Module({
imports: [ListingsModule,FirebaseAdminModule], imports: [ListingsModule],
controllers: [ImageController], controllers: [ImageController],
providers: [ImageService, FileService, SelectOptionsService], providers: [ImageService, FileService, SelectOptionsService],
}) })

View File

@ -1,40 +0,0 @@
// src/interceptors/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
constructor(private readonly cls: ClsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const ip = this.cls.get('ip') || 'unknown';
const countryCode = this.cls.get('countryCode') || 'unknown';
const username = this.cls.get('email') || 'unknown';
const method = request.method;
const url = request.originalUrl;
const start = Date.now();
this.logger.log(`Entering ${method} ${url} from ${ip} (${countryCode})- User: ${username}`);
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
let logMessage = `${method} ${url} - ${duration}ms - IP: ${ip} - User: ${username}`;
if (method === 'POST' || method === 'PUT') {
const body = JSON.stringify(request.body);
logMessage += ` - Incoming Body: ${body}`;
}
this.logger.log(logMessage);
}),
);
}
}

View File

@ -1,29 +0,0 @@
// src/interceptors/user.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import { Observable } from 'rxjs';
@Injectable()
export class UserInterceptor implements NestInterceptor {
private readonly logger = new Logger(UserInterceptor.name);
constructor(private readonly cls: ClsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// Überprüfe, ob der Benutzer authentifiziert ist
if (request.user && request.user.email) {
try {
this.cls.set('email', request.user.email);
this.logger.log(`CLS context gesetzt: EMail=${request.user.email}`);
} catch (error) {
this.logger.error('Fehler beim Setzen der EMail im CLS-Kontext', error);
}
} else {
this.logger.log('Kein authentifizierter Benutzer gefunden');
}
return next.handle();
}
}

View File

@ -1,20 +0,0 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// The FirebaseAuthGuard should run before this guard
// and populate the request.user object
if (!request.user) {
throw new ForbiddenException('User not authenticated');
}
if (request.user.role !== 'admin') {
throw new ForbiddenException('Requires admin privileges');
}
return true;
}
}

View File

@ -1,42 +0,0 @@
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import * as admin from 'firebase-admin';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Missing or invalid authorization token');
}
const token = authHeader.split('Bearer ')[1];
try {
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
// Check if email is verified (optional but recommended)
if (!decodedToken.email_verified) {
throw new UnauthorizedException('Email not verified');
}
// Add the user to the request
request.user = {
uid: decodedToken.uid,
email: decodedToken.email,
role: decodedToken.role || null,
// Add other user info as needed
};
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
}

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

@ -1,21 +0,0 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class LocalhostGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const ip = request.ip;
// Liste der erlaubten IPs
const allowedIPs = ['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1'];
if (!allowedIPs.includes(ip)) {
console.warn(`Versuchter Zugriff von unerlaubter IP: ${ip}`);
throw new ForbiddenException('Dieser Endpunkt kann nur lokal aufgerufen werden');
}
return true;
}
}

View File

@ -1,76 +0,0 @@
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
import * as admin from 'firebase-admin';
@Injectable()
export class OptionalAuthGuard implements CanActivate {
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
//throw new UnauthorizedException('Missing or invalid authorization token');
return true;
}
const token = authHeader.split('Bearer ')[1];
try {
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
// Check if email is verified (optional but recommended)
if (!decodedToken.email_verified) {
//throw new UnauthorizedException('Email not verified');
return true;
}
// Add the user to the request
request.user = {
uid: decodedToken.uid,
email: decodedToken.email,
role: decodedToken.role || null,
// Add other user info as needed
};
return true;
} catch (error) {
//throw new UnauthorizedException('Invalid token');
return true;
}
}
}
// import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
// import * as admin from 'firebase-admin';
// @Injectable()
// export class OptionalAuthGuard implements CanActivate {
// constructor(
// @Inject('FIREBASE_ADMIN')
// private readonly firebaseAdmin: typeof admin,
// ) {}
// async canActivate(context: ExecutionContext): Promise<boolean> {
// const request = context.switchToHttp().getRequest<Request>();
// const token = this.extractTokenFromHeader(request);
// if (!token) {
// return true;
// }
// try {
// const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
// request['user'] = decodedToken;
// return true;
// } catch (error) {
// //throw new UnauthorizedException('Invalid token');
// request['user'] = null;
// return true;
// }
// }
// private extractTokenFromHeader(request: Request): string | undefined {
// const [type, token] = request.headers['authorization']?.split(' ') ?? [];
// return type === 'Bearer' ? token : undefined;
// }
// }

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, firstname: payload.given_name, lastname: payload.family_name, username: payload.preferred_username, roles: payload.realm_access?.roles };
this.logger.info(`JWT User: ${JSON.stringify(result)}`); // Debugging: JWT Payload anzeigen
return result;
}
}

View File

@ -1,9 +1,8 @@
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Inject, Post } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard'; import { UserListingCriteria } from 'src/models/main.model.js';
import { UserListingCriteria } from 'src/models/main.model';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service.js';
@Controller('listings/professionals_brokers') @Controller('listings/professionals_brokers')
export class BrokerListingsController { export class BrokerListingsController {
@ -12,9 +11,8 @@ export class BrokerListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@UseGuards(OptionalAuthGuard)
@Post('search') @Post('search')
async find(@Body() criteria: UserListingCriteria): Promise<any> { find(@Body() criteria: UserListingCriteria): any {
return await this.userService.searchUserListings(criteria); return this.userService.searchUserListings(criteria);
} }
} }

View File

@ -1,207 +1,141 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { and, arrayContains, asc, count, desc, eq, gte, inArray, lte, or, SQL, sql } from 'drizzle-orm'; import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import * as schema from '../drizzle/schema'; import * as schema from '../drizzle/schema.js';
import { businesses_json, PG_CONNECTION, users_json } from '../drizzle/schema'; import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
import { GeoService } from '../geo/geo.service'; import { FileService } from '../file/file.service.js';
import { BusinessListing, BusinessListingSchema } from '../models/db.model'; import { GeoService } from '../geo/geo.service.js';
import { BusinessListingCriteria, JwtUser } from '../models/main.model'; import { BusinessListing, BusinessListingSchema } from '../models/db.model.js';
import { getDistanceQuery, splitName } from '../utils'; import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery } from '../utils.js';
@Injectable() @Injectable()
export class BusinessListingService { export class BusinessListingService {
constructor( constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>, @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService?: FileService,
private geoService?: GeoService, private geoService?: GeoService,
) {} ) {}
private getWhereConditions(criteria: BusinessListingCriteria, user: JwtUser): SQL[] { private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
const whereConditions: SQL[] = []; const whereConditions: SQL[] = [];
if (criteria.city && criteria.searchType === 'exact') { if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(sql`(${businesses_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`); whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
whereConditions.push(sql`${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
} }
if (criteria.types && criteria.types.length > 0) { if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(sql`${businesses_json.data}->>'type'`, criteria.types)); whereConditions.push(inArray(businesses.type, criteria.types));
} }
if (criteria.state) { if (criteria.state) {
whereConditions.push(sql`(${businesses_json.data}->'location'->>'state') = ${criteria.state}`); whereConditions.push(eq(businesses.state, criteria.state));
} }
if (criteria.minPrice) { if (criteria.minPrice) {
whereConditions.push(gte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.minPrice)); whereConditions.push(gte(businesses.price, criteria.minPrice));
} }
if (criteria.maxPrice) { if (criteria.maxPrice) {
whereConditions.push(lte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.maxPrice)); whereConditions.push(lte(businesses.price, criteria.maxPrice));
} }
if (criteria.minRevenue) { if (criteria.minRevenue) {
whereConditions.push(gte(sql`(${businesses_json.data}->>'salesRevenue')::double precision`, criteria.minRevenue)); whereConditions.push(gte(businesses.salesRevenue, criteria.minRevenue));
} }
if (criteria.maxRevenue) { if (criteria.maxRevenue) {
whereConditions.push(lte(sql`(${businesses_json.data}->>'salesRevenue')::double precision`, criteria.maxRevenue)); whereConditions.push(lte(businesses.salesRevenue, criteria.maxRevenue));
} }
if (criteria.minCashFlow) { if (criteria.minCashFlow) {
whereConditions.push(gte(sql`(${businesses_json.data}->>'cashFlow')::double precision`, criteria.minCashFlow)); whereConditions.push(gte(businesses.cashFlow, criteria.minCashFlow));
} }
if (criteria.maxCashFlow) { if (criteria.maxCashFlow) {
whereConditions.push(lte(sql`(${businesses_json.data}->>'cashFlow')::double precision`, criteria.maxCashFlow)); whereConditions.push(lte(businesses.cashFlow, criteria.maxCashFlow));
} }
if (criteria.minNumberEmployees) { if (criteria.minNumberEmployees) {
whereConditions.push(gte(sql`(${businesses_json.data}->>'employees')::integer`, criteria.minNumberEmployees)); whereConditions.push(gte(businesses.employees, criteria.minNumberEmployees));
} }
if (criteria.maxNumberEmployees) { if (criteria.maxNumberEmployees) {
whereConditions.push(lte(sql`(${businesses_json.data}->>'employees')::integer`, criteria.maxNumberEmployees)); whereConditions.push(lte(businesses.employees, criteria.maxNumberEmployees));
} }
if (criteria.establishedMin) { if (criteria.establishedSince) {
whereConditions.push(gte(sql`(${businesses_json.data}->>'established')::integer`, criteria.establishedMin)); whereConditions.push(gte(businesses.established, criteria.establishedSince));
}
if (criteria.establishedUntil) {
whereConditions.push(lte(businesses.established, criteria.establishedUntil));
} }
if (criteria.realEstateChecked) { if (criteria.realEstateChecked) {
whereConditions.push(eq(sql`(${businesses_json.data}->>'realEstateIncluded')::boolean`, criteria.realEstateChecked)); whereConditions.push(eq(businesses.realEstateIncluded, criteria.realEstateChecked));
} }
if (criteria.leasedLocation) { if (criteria.leasedLocation) {
whereConditions.push(eq(sql`(${businesses_json.data}->>'leasedLocation')::boolean`, criteria.leasedLocation)); whereConditions.push(eq(businesses.leasedLocation, criteria.leasedLocation));
} }
if (criteria.franchiseResale) { if (criteria.franchiseResale) {
whereConditions.push(eq(sql`(${businesses_json.data}->>'franchiseResale')::boolean`, criteria.franchiseResale)); whereConditions.push(eq(businesses.franchiseResale, criteria.franchiseResale));
} }
if (criteria.title) { if (criteria.title) {
whereConditions.push(sql`(${businesses_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${businesses_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`); whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`)));
}
if (criteria.brokerName) {
const { firstname, lastname } = splitName(criteria.brokerName);
if (firstname === lastname) {
whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`);
} else {
whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`);
}
}
if (criteria.email) {
whereConditions.push(eq(users_json.email, criteria.email));
}
if (user?.role !== 'admin') {
whereConditions.push(or(eq(businesses_json.email, user?.email), sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`));
}
whereConditions.push(and(sql`(${users_json.data}->>'customerType') = 'professional'`, sql`(${users_json.data}->>'customerSubType') = 'broker'`));
return whereConditions;
} }
if (criteria.brokerName) {
whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`)));
}
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
return whereConditions;
}
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) { async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
const query = this.conn const query = this.conn
.select({ .select({
business: businesses_json, business: businesses,
brokerFirstName: sql`${users_json.data}->>'firstname'`.as('brokerFirstName'), brokerFirstName: schema.users.firstname,
brokerLastName: sql`${users_json.data}->>'lastname'`.as('brokerLastName'), brokerLastName: schema.users.lastname,
}) })
.from(businesses_json) .from(businesses)
.leftJoin(users_json, eq(businesses_json.email, users_json.email)); .leftJoin(schema.users, eq(businesses.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria, user); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
const whereClause = and(...whereConditions); const whereClause = and(...whereConditions);
query.where(whereClause); query.where(whereClause);
} }
// Sortierung
switch (criteria.sortBy) {
case 'priceAsc':
query.orderBy(asc(sql`(${businesses_json.data}->>'price')::double precision`));
break;
case 'priceDesc':
query.orderBy(desc(sql`(${businesses_json.data}->>'price')::double precision`));
break;
case 'srAsc':
query.orderBy(asc(sql`(${businesses_json.data}->>'salesRevenue')::double precision`));
break;
case 'srDesc':
query.orderBy(desc(sql`(${businesses_json.data}->>'salesRevenue')::double precision`));
break;
case 'cfAsc':
query.orderBy(asc(sql`(${businesses_json.data}->>'cashFlow')::double precision`));
break;
case 'cfDesc':
query.orderBy(desc(sql`(${businesses_json.data}->>'cashFlow')::double precision`));
break;
case 'creationDateFirst':
query.orderBy(asc(sql`${businesses_json.data}->>'created'`));
break;
case 'creationDateLast':
query.orderBy(desc(sql`${businesses_json.data}->>'created'`));
break;
default: {
// NEU (created < 14 Tage) > UPDATED (updated < 14 Tage) > Rest
const recencyRank = sql`
CASE
WHEN ((${businesses_json.data}->>'created')::timestamptz >= (now() - interval '14 days')) THEN 2
WHEN ((${businesses_json.data}->>'updated')::timestamptz >= (now() - interval '14 days')) THEN 1
ELSE 0
END
`;
// Innerhalb der Gruppe:
// NEW → created DESC
// UPDATED → updated DESC
// Rest → created DESC
const groupTimestamp = sql`
CASE
WHEN ((${businesses_json.data}->>'created')::timestamptz >= (now() - interval '14 days'))
THEN (${businesses_json.data}->>'created')::timestamptz
WHEN ((${businesses_json.data}->>'updated')::timestamptz >= (now() - interval '14 days'))
THEN (${businesses_json.data}->>'updated')::timestamptz
ELSE (${businesses_json.data}->>'created')::timestamptz
END
`;
query.orderBy(desc(recencyRank), desc(groupTimestamp), desc(sql`(${businesses_json.data}->>'created')::timestamptz`));
break;
}
}
// Paginierung // Paginierung
query.limit(length).offset(start); query.limit(length).offset(start);
const data = await query; const data = await query;
const totalCount = await this.getBusinessListingsCount(criteria, user); const totalCount = await this.getBusinessListingsCount(criteria);
const results = data.map(r => ({ const results = data.map(r => r.business).map(r => convertDrizzleBusinessToBusiness(r));
id: r.business.id,
email: r.business.email,
...(r.business.data as BusinessListing),
brokerFirstName: r.brokerFirstName,
brokerLastName: r.brokerLastName,
}));
return { return {
results, results,
totalCount, totalCount,
}; };
} }
async getBusinessListingsCount(criteria: BusinessListingCriteria, user: JwtUser): Promise<number> { async getBusinessListingsCount(criteria: BusinessListingCriteria): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(businesses_json).leftJoin(users_json, eq(businesses_json.email, users_json.email)); const countQuery = this.conn.select({ value: count() }).from(businesses).leftJoin(schema.users, eq(businesses.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria, user); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
const whereClause = and(...whereConditions); const whereClause = and(...whereConditions);
@ -213,107 +147,80 @@ export class BusinessListingService {
} }
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> { async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
const conditions = []; let result = await this.conn
if (user?.role !== 'admin') {
conditions.push(or(eq(businesses_json.email, user?.email), sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`));
}
conditions.push(eq(businesses_json.id, id));
const result = await this.conn
.select() .select()
.from(businesses_json) .from(businesses)
.where(and(...conditions)); .where(and(sql`${businesses.id} = ${id}`));
if (result.length > 0) { result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return { id: result[0].id, email: result[0].email, ...(result[0].data as BusinessListing) } as BusinessListing; return convertDrizzleBusinessToBusiness(result[0]) as BusinessListing;
} else {
throw new BadRequestException(`No entry available for ${id}`);
}
} }
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> { async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const conditions = []; const conditions = [];
conditions.push(eq(businesses_json.email, email)); conditions.push(eq(businesses.imageName, emailToDirName(email)));
if (email !== user?.email && user?.role !== 'admin') { if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`); conditions.push(ne(businesses.draft, true));
} }
const listings = await this.conn const listings = (await this.conn
.select() .select()
.from(businesses_json) .from(businesses)
.where(and(...conditions)); .where(and(...conditions))) as BusinessListing[];
return listings.map(l => ({ id: l.id, email: l.email, ...(l.data as BusinessListing) }) as BusinessListing);
} return listings.map(l => convertDrizzleBusinessToBusiness(l));
async findFavoriteListings(user: JwtUser): Promise<BusinessListing[]> {
const userFavorites = await this.conn
.select()
.from(businesses_json)
.where(arrayContains(sql`${businesses_json.data}->>'favoritesForUser'`, [user.email]));
return userFavorites.map(l => ({ id: l.id, email: l.email, ...(l.data as BusinessListing) }) as BusinessListing);
} }
// #### CREATE ########################################
async createListing(data: BusinessListing): Promise<BusinessListing> { async createListing(data: BusinessListing): Promise<BusinessListing> {
try { try {
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
data.updated = new Date(); data.updated = new Date();
BusinessListingSchema.parse(data); const validatedBusinessListing = BusinessListingSchema.parse(data);
const { id, email, ...rest } = data; const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
const convertedBusinessListing = { email, data: rest }; const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
const [createdListing] = await this.conn.insert(businesses_json).values(convertedBusinessListing).returning(); return convertDrizzleBusinessToBusiness(createdListing);
return { id: createdListing.id, email: createdListing.email, ...(createdListing.data as BusinessListing) };
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const filteredErrors = error.errors const formattedErrors = error.errors.map(err => ({
.map(item => ({ field: err.path.join('.'),
...item, message: err.message,
field: item.path[0], }));
})) throw new BadRequestException(formattedErrors);
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }
} }
// #### UPDATE Business ########################################
async updateBusinessListing(id: string, data: BusinessListing, user: JwtUser): Promise<BusinessListing> { async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> {
try { try {
const [existingListing] = await this.conn.select().from(businesses_json).where(eq(businesses_json.id, id));
if (!existingListing) {
throw new NotFoundException(`Business listing with id ${id} not found`);
}
data.updated = new Date(); data.updated = new Date();
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
if (existingListing.email === user?.email) { const validatedBusinessListing = BusinessListingSchema.parse(data);
data.favoritesForUser = (<BusinessListing>existingListing.data).favoritesForUser || []; const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
} const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
BusinessListingSchema.parse(data); return convertDrizzleBusinessToBusiness(updateListing);
const { id: _, email, ...rest } = data;
const convertedBusinessListing = { email, data: rest };
const [updateListing] = await this.conn.update(businesses_json).set(convertedBusinessListing).where(eq(businesses_json.id, id)).returning();
return { id: updateListing.id, email: updateListing.email, ...(updateListing.data as BusinessListing) };
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const filteredErrors = error.errors const formattedErrors = error.errors.map(err => ({
.map(item => ({ field: err.path.join('.'),
...item, message: err.message,
field: item.path[0], }));
})) throw new BadRequestException(formattedErrors);
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }
} }
// #### DELETE ########################################
async deleteListing(id: string): Promise<void> { async deleteListing(id: string): Promise<void> {
await this.conn.delete(businesses_json).where(eq(businesses_json.id, id)); await this.conn.delete(businesses).where(eq(businesses.id, id));
} }
// ##############################################################
async deleteFavorite(id: string, user: JwtUser): Promise<void> { // States
await this.conn // ##############################################################
.update(businesses_json) async getStates(): Promise<any[]> {
.set({ return await this.conn
data: sql`jsonb_set(${businesses_json.data}, '{favoritesForUser}', array_remove((${businesses_json.data}->>'favoritesForUser')::jsonb, ${user.email}))`, .select({ state: businesses.state, count: sql<number>`count(${businesses.id})`.mapWith(Number) })
}) .from(businesses)
.where(eq(businesses_json.id, id)); .groupBy(sql`${businesses.state}`)
.orderBy(sql`count desc`);
} }
} }

View File

@ -1,12 +1,10 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { AuthGuard } from 'src/jwt-auth/auth.guard'; import { BusinessListing } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard'; import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
import { BusinessListing } from '../models/db.model'; import { BusinessListingService } from './business-listing.service.js';
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
import { BusinessListingService } from './business-listing.service';
@Controller('listings/business') @Controller('listings/business')
export class BusinessListingsController { export class BusinessListingsController {
@ -15,54 +13,49 @@ export class BusinessListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@UseGuards(OptionalAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Get(':id') @Get(':id')
async findById(@Request() req, @Param('id') id: string): Promise<any> { findById(@Request() req, @Param('id') id: string): any {
return await this.listingsService.findBusinessesById(id, req.user as JwtUser); return this.listingsService.findBusinessesById(id, req.user as JwtUser);
} }
@UseGuards(AuthGuard)
@Get('favorites/all') @UseGuards(OptionalJwtAuthGuard)
async findFavorites(@Request() req): Promise<any> {
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
}
@UseGuards(OptionalAuthGuard)
@Get('user/:userid') @Get('user/:userid')
async findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> { findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
return await this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser); return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
} }
@UseGuards(OptionalAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Post('find') @Post('find')
async find(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<any> { find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
return await this.listingsService.searchBusinessListings(criteria, req.user as JwtUser); return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
} }
@UseGuards(OptionalAuthGuard)
@Post('findTotal') @Post('findTotal')
async findTotal(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<number> { findTotal(@Body() criteria: BusinessListingCriteria): Promise<number> {
return await this.listingsService.getBusinessListingsCount(criteria, req.user as JwtUser); return this.listingsService.getBusinessListingsCount(criteria);
} }
// @UseGuards(OptionalJwtAuthGuard)
// @Post('search')
// search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
// return this.listingsService.searchBusinessListings(criteria.prompt);
// }
@UseGuards(OptionalAuthGuard)
@Post() @Post()
async create(@Body() listing: any) { create(@Body() listing: any) {
return await this.listingsService.createListing(listing); this.logger.info(`Save Listing`);
return this.listingsService.createListing(listing);
} }
@UseGuards(OptionalAuthGuard)
@Put() @Put()
async update(@Request() req, @Body() listing: any) { update(@Body() listing: any) {
return await this.listingsService.updateBusinessListing(listing.id, listing, req.user as JwtUser); this.logger.info(`Save Listing`);
return this.listingsService.updateBusinessListing(listing.id, listing);
} }
@Delete(':id')
@UseGuards(OptionalAuthGuard) deleteById(@Param('id') id: string) {
@Delete('listing/:id') this.listingsService.deleteListing(id);
async deleteById(@Param('id') id: string) {
await this.listingsService.deleteListing(id);
} }
@Get('states/all')
@UseGuards(AuthGuard) getStates(): any {
@Delete('favorite/:id') return this.listingsService.getStates();
async deleteFavorite(@Request() req, @Param('id') id: string) {
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
} }
} }

View File

@ -1,13 +1,11 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service.js';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { CommercialPropertyListing } from '../models/db.model'; import { CommercialPropertyListing } from '../models/db.model';
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model'; import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js';
import { CommercialPropertyService } from './commercial-property.service'; import { CommercialPropertyService } from './commercial-property.service.js';
@Controller('listings/commercialProperty') @Controller('listings/commercialProperty')
export class CommercialPropertyListingsController { export class CommercialPropertyListingsController {
@ -17,56 +15,43 @@ export class CommercialPropertyListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@UseGuards(OptionalAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Get(':id') @Get(':id')
async findById(@Request() req, @Param('id') id: string): Promise<any> { findById(@Request() req, @Param('id') id: string): any {
return await this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser); return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
} }
@UseGuards(AuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Get('favorites/all')
async findFavorites(@Request() req): Promise<any> {
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
}
@UseGuards(OptionalAuthGuard)
@Get('user/:email') @Get('user/:email')
async findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> { findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
return await this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser); return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard)
@UseGuards(OptionalAuthGuard)
@Post('find') @Post('find')
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> { async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser); return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
} }
@UseGuards(OptionalAuthGuard)
@Post('findTotal') @Post('findTotal')
async findTotal(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<number> { findTotal(@Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
return await this.listingsService.getCommercialPropertiesCount(criteria, req.user as JwtUser); return this.listingsService.getCommercialPropertiesCount(criteria);
}
@Get('states/all')
getStates(): any {
return this.listingsService.getStates();
} }
@UseGuards(OptionalAuthGuard)
@Post() @Post()
async create(@Body() listing: any) { async create(@Body() listing: any) {
this.logger.info(`Save Listing`);
return await this.listingsService.createListing(listing); return await this.listingsService.createListing(listing);
} }
@UseGuards(OptionalAuthGuard)
@Put() @Put()
async update(@Request() req, @Body() listing: any) { async update(@Body() listing: any) {
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing, req.user as JwtUser); this.logger.info(`Save Listing`);
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
} }
@Delete(':id/:imagePath')
@UseGuards(OptionalAuthGuard) deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
@Delete('listing/:id/:imagePath') this.listingsService.deleteListing(id);
async deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
await this.listingsService.deleteListing(id);
this.fileService.deleteDirectoryIfExists(imagePath); this.fileService.deleteDirectoryIfExists(imagePath);
} }
@UseGuards(AuthGuard)
@Delete('favorite/:id')
async deleteFavorite(@Request() req, @Param('id') id: string) {
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
}
} }

View File

@ -1,16 +1,16 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { and, arrayContains, asc, count, desc, eq, gte, inArray, lte, or, SQL, sql } from 'drizzle-orm'; import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import * as schema from '../drizzle/schema'; import * as schema from '../drizzle/schema.js';
import { commercials_json, PG_CONNECTION } from '../drizzle/schema'; import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service'; import { GeoService } from '../geo/geo.service.js';
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model'; import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js';
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model'; import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { getDistanceQuery } from '../utils'; import { convertCommercialToDrizzleCommercial, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils.js';
@Injectable() @Injectable()
export class CommercialPropertyService { export class CommercialPropertyService {
@ -20,86 +20,65 @@ export class CommercialPropertyService {
private fileService?: FileService, private fileService?: FileService,
private geoService?: GeoService, private geoService?: GeoService,
) {} ) {}
private getWhereConditions(criteria: CommercialPropertyListingCriteria, user: JwtUser): SQL[] { private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
const whereConditions: SQL[] = []; const whereConditions: SQL[] = [];
if (criteria.city && criteria.searchType === 'exact') { if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(sql`(${commercials_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`); whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
whereConditions.push(sql`${getDistanceQuery(commercials_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
} }
if (criteria.types && criteria.types.length > 0) { if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(sql`${commercials_json.data}->>'type'`, criteria.types)); whereConditions.push(inArray(schema.commercials.type, criteria.types));
} }
if (criteria.state) { if (criteria.state) {
whereConditions.push(sql`(${commercials_json.data}->'location'->>'state') = ${criteria.state}`); whereConditions.push(eq(schema.commercials.state, criteria.state));
} }
if (criteria.minPrice) { if (criteria.minPrice) {
whereConditions.push(gte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.minPrice)); whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
} }
if (criteria.maxPrice) { if (criteria.maxPrice) {
whereConditions.push(lte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.maxPrice)); whereConditions.push(lte(schema.commercials.price, criteria.maxPrice));
} }
if (criteria.title) { if (criteria.title) {
whereConditions.push(sql`(${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`); whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
} }
if (user?.role !== 'admin') { whereConditions.push(and(eq(schema.users.customerType, 'professional')));
whereConditions.push(or(eq(commercials_json.email, user?.email), sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`));
}
// whereConditions.push(and(eq(schema.users.customerType, 'professional')));
return whereConditions; return whereConditions;
} }
// #### Find by criteria ######################################## // #### Find by criteria ########################################
async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> { async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
const query = this.conn.select({ commercial: commercials_json }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email)); const query = this.conn.select({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria, user); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
const whereClause = and(...whereConditions); const whereClause = and(...whereConditions);
query.where(whereClause); query.where(whereClause);
} }
// Sortierung
switch (criteria.sortBy) {
case 'priceAsc':
query.orderBy(asc(sql`(${commercials_json.data}->>'price')::double precision`));
break;
case 'priceDesc':
query.orderBy(desc(sql`(${commercials_json.data}->>'price')::double precision`));
break;
case 'creationDateFirst':
query.orderBy(asc(sql`${commercials_json.data}->>'created'`));
break;
case 'creationDateLast':
query.orderBy(desc(sql`${commercials_json.data}->>'created'`));
break;
default:
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
break;
}
// Paginierung // Paginierung
query.limit(length).offset(start); query.limit(length).offset(start);
const data = await query; const data = await query;
const results = data.map(r => ({ id: r.commercial.id, email: r.commercial.email, ...(r.commercial.data as CommercialPropertyListing) })); const results = data.map(r => r.commercial).map(r => convertDrizzleCommercialToCommercial(r));
const totalCount = await this.getCommercialPropertiesCount(criteria, user); const totalCount = await this.getCommercialPropertiesCount(criteria);
return { return {
results, results,
totalCount, totalCount,
}; };
} }
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<number> { async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email)); const countQuery = this.conn.select({ value: count() }).from(schema.commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria, user); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
const whereClause = and(...whereConditions); const whereClause = and(...whereConditions);
@ -112,123 +91,77 @@ export class CommercialPropertyService {
// #### Find by ID ######################################## // #### Find by ID ########################################
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> { async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
const conditions = []; let result = await this.conn
if (user?.role !== 'admin') {
conditions.push(or(eq(commercials_json.email, user?.email), sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`));
}
conditions.push(eq(commercials_json.id, id));
const result = await this.conn
.select() .select()
.from(commercials_json) .from(commercials)
.where(and(...conditions)); .where(and(sql`${commercials.id} = ${id}`));
if (result.length > 0) { result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing; return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
} else {
throw new BadRequestException(`No entry available for ${id}`);
}
} }
// #### Find by User EMail ######################################## // #### Find by User EMail ########################################
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> { async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
const conditions = []; const conditions = [];
conditions.push(eq(commercials_json.email, email)); conditions.push(eq(commercials.imagePath, emailToDirName(email)));
if (email !== user?.email && user?.role !== 'admin') { if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`); conditions.push(ne(commercials.draft, true));
} }
const listings = await this.conn const listings = (await this.conn
.select() .select()
.from(commercials_json) .from(commercials)
.where(and(...conditions)); .where(and(...conditions))) as CommercialPropertyListing[];
return listings.map(l => ({ id: l.id, email: l.email, ...(l.data as CommercialPropertyListing) }) as CommercialPropertyListing); return listings.map(l => convertDrizzleCommercialToCommercial(l)) as CommercialPropertyListing[];
}
// #### Find Favorites ########################################
async findFavoriteListings(user: JwtUser): Promise<CommercialPropertyListing[]> {
const userFavorites = await this.conn
.select()
.from(commercials_json)
.where(arrayContains(sql`${commercials_json.data}->>'favoritesForUser'`, [user.email]));
return userFavorites.map(l => ({ id: l.id, email: l.email, ...(l.data as CommercialPropertyListing) }) as CommercialPropertyListing);
} }
// #### Find by imagePath ######################################## // #### Find by imagePath ########################################
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> { async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
const result = await this.conn const result = await this.conn
.select() .select()
.from(commercials_json) .from(commercials)
.where(and(sql`(${commercials_json.data}->>'imagePath') = ${imagePath}`, sql`(${commercials_json.data}->>'serialId')::integer = ${serial}`)); .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
if (result.length > 0) { return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing;
}
} }
// #### CREATE ######################################## // #### CREATE ########################################
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> { async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try { try {
// Hole die nächste serialId von der Sequence
const sequenceResult = await this.conn.execute(sql`SELECT nextval('commercials_json_serial_id_seq') AS serialid`);
// Prüfe, ob ein gültiger Wert zurückgegeben wurde
if (!sequenceResult.rows || !sequenceResult.rows[0] || sequenceResult.rows[0].serialid === undefined) {
throw new Error('Failed to retrieve serialId from sequence commercials_json_serial_id_seq');
}
const serialId = Number(sequenceResult.rows[0].serialid); // Konvertiere BIGINT zu Number
if (isNaN(serialId)) {
throw new Error('Invalid serialId received from sequence');
}
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
data.updated = new Date(); data.updated = new Date();
data.serialId = Number(serialId); const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
CommercialPropertyListingSchema.parse(data); const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
const { id, email, ...rest } = data; const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning();
const convertedCommercialPropertyListing = { email, data: rest }; return convertDrizzleCommercialToCommercial(createdListing);
const [createdListing] = await this.conn.insert(commercials_json).values(convertedCommercialPropertyListing).returning();
return { id: createdListing.id, email: createdListing.email, ...(createdListing.data as CommercialPropertyListing) };
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const filteredErrors = error.errors const formattedErrors = error.errors.map(err => ({
.map(item => ({ field: err.path.join('.'),
...item, message: err.message,
field: item.path[0], }));
})) throw new BadRequestException(formattedErrors);
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }
} }
// #### UPDATE CommercialProps ######################################## // #### UPDATE CommercialProps ########################################
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing, user: JwtUser): Promise<CommercialPropertyListing> { async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try { try {
const [existingListing] = await this.conn.select().from(commercials_json).where(eq(commercials_json.id, id));
if (!existingListing) {
throw new NotFoundException(`Business listing with id ${id} not found`);
}
data.updated = new Date(); data.updated = new Date();
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
if (existingListing.email === user?.email || !user) { const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
data.favoritesForUser = (<CommercialPropertyListing>existingListing.data).favoritesForUser || [];
}
CommercialPropertyListingSchema.parse(data);
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId)); const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
const difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x))); let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
if (difference.length > 0) { if (difference.length > 0) {
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`); this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
data.imageOrder = imageOrder; data.imageOrder = imageOrder;
} }
const { id: _, email, ...rest } = data; const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
const convertedCommercialPropertyListing = { email, data: rest }; const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning();
const [updateListing] = await this.conn.update(commercials_json).set(convertedCommercialPropertyListing).where(eq(commercials_json.id, id)).returning(); return convertDrizzleCommercialToCommercial(updateListing);
return { id: updateListing.id, email: updateListing.email, ...(updateListing.data as CommercialPropertyListing) };
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const filteredErrors = error.errors const formattedErrors = error.errors.map(err => ({
.map(item => ({ field: err.path.join('.'),
...item, message: err.message,
field: item.path[0], }));
})) throw new BadRequestException(formattedErrors);
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }
@ -237,29 +170,30 @@ export class CommercialPropertyService {
// Images for commercial Properties // Images for commercial Properties
// ############################################################## // ##############################################################
async deleteImage(imagePath: string, serial: string, name: string) { async deleteImage(imagePath: string, serial: string, name: string) {
const listing = await this.findByImagePath(imagePath, serial); const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
const index = listing.imageOrder.findIndex(im => im === name); const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) { if (index > -1) {
listing.imageOrder.splice(index, 1); listing.imageOrder.splice(index, 1);
await this.updateCommercialPropertyListing(listing.id, listing, null); await this.updateCommercialPropertyListing(listing.id, listing);
} }
} }
async addImage(imagePath: string, serial: string, imagename: string) { async addImage(imagePath: string, serial: string, imagename: string) {
const listing = await this.findByImagePath(imagePath, serial); const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
listing.imageOrder.push(imagename); listing.imageOrder.push(imagename);
await this.updateCommercialPropertyListing(listing.id, listing, null); await this.updateCommercialPropertyListing(listing.id, listing);
} }
// #### DELETE ######################################## // #### DELETE ########################################
async deleteListing(id: string): Promise<void> { async deleteListing(id: string): Promise<void> {
await this.conn.delete(commercials_json).where(eq(commercials_json.id, id)); await this.conn.delete(commercials).where(eq(commercials.id, id));
} }
// #### DELETE Favorite ################################### // ##############################################################
async deleteFavorite(id: string, user: JwtUser): Promise<void> { // States
await this.conn // ##############################################################
.update(commercials_json) async getStates(): Promise<any[]> {
.set({ return await this.conn
data: sql`jsonb_set(${commercials_json.data}, '{favoritesForUser}', array_remove((${commercials_json.data}->>'favoritesForUser')::jsonb, ${user.email}))`, .select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
}) .from(commercials)
.where(eq(commercials_json.id, id)); .groupBy(sql`${commercials.state}`)
.orderBy(sql`count desc`);
} }
} }

View File

@ -1,21 +1,20 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module.js';
import { DrizzleModule } from '../drizzle/drizzle.module'; import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service.js';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service.js';
import { BrokerListingsController } from './broker-listings.controller'; import { BrokerListingsController } from './broker-listings.controller.js';
import { BusinessListingsController } from './business-listings.controller'; import { BusinessListingsController } from './business-listings.controller.js';
import { CommercialPropertyListingsController } from './commercial-property-listings.controller'; import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module'; import { GeoModule } from '../geo/geo.module.js';
import { GeoModule } from '../geo/geo.module'; import { GeoService } from '../geo/geo.service.js';
import { GeoService } from '../geo/geo.service'; import { BusinessListingService } from './business-listing.service.js';
import { BusinessListingService } from './business-listing.service'; import { CommercialPropertyService } from './commercial-property.service.js';
import { CommercialPropertyService } from './commercial-property.service'; import { UnknownListingsController } from './unknown-listings.controller.js';
import { UnknownListingsController } from './unknown-listings.controller';
@Module({ @Module({
imports: [DrizzleModule, AuthModule, GeoModule,FirebaseAdminModule], imports: [DrizzleModule, AuthModule, GeoModule],
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController], controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService], providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
exports: [BusinessListingService, CommercialPropertyService], exports: [BusinessListingService, CommercialPropertyService],

View File

@ -1,25 +1,18 @@
import { Controller, Get, Inject, Param, Request, UseGuards } from '@nestjs/common'; import { Controller, Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { BusinessListingService } from './business-listing.service';
import { CommercialPropertyService } from './commercial-property.service';
@Controller('listings/undefined') @Controller('listings/undefined')
export class UnknownListingsController { export class UnknownListingsController {
constructor( constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
private readonly businessListingsService: BusinessListingService,
private readonly propertyListingsService: CommercialPropertyService,
) {}
@UseGuards(OptionalAuthGuard) // @Get(':id')
@Get(':id') // async findById(@Param('id') id: string): Promise<any> {
async findById(@Request() req, @Param('id') id: string): Promise<any> { // const result = await this.listingsService.findById(id, businesses);
try { // if (result) {
return await this.businessListingsService.findBusinessesById(id, req.user); // return result;
} catch (error) { // } else {
return await this.propertyListingsService.findCommercialPropertiesById(id, req.user); // return await this.listingsService.findById(id, commercials);
} // }
} // }
} }

View File

@ -1,19 +0,0 @@
import { Body, Controller, Inject, Post, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { Logger } from 'winston';
import { LogMessage } from '../models/main.model';
@Controller('log')
export class LogController {
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
@UseGuards(OptionalAuthGuard)
@Post()
log(@Request() req, @Body() message: LogMessage) {
if (message.severity === 'info') {
this.logger.info(message.text);
} else {
this.logger.error(message.text);
}
}
}

View File

@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
import { LogController } from './log.controller';
@Module({
imports: [FirebaseAdminModule],
controllers: [LogController],
})
export class LogModule {}

View File

@ -1,14 +0,0 @@
import { registerAs } from '@nestjs/config';
export default registerAs('mail', () => ({
host: 'email-smtp.us-east-2.amazonaws.com',
port: 587,
secure: false,
auth: {
user: process.env.AMAZON_USER,
pass: process.env.AMAZON_PASSWORD,
},
defaults: {
from: '"No Reply" <noreply@example.com>',
},
}));

View File

@ -1,43 +1,16 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { Body, Controller, Post } from '@nestjs/common';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { ShareByEMail, User } from 'src/models/db.model';
import { ErrorResponse, MailInfo } from '../models/main.model'; import { ErrorResponse, MailInfo } from '../models/main.model';
import { MailService } from './mail.service'; import { MailService } from './mail.service.js';
@Controller('mail') @Controller('mail')
export class MailController { export class MailController {
constructor(private mailService: MailService) {} constructor(private mailService: MailService) {}
@UseGuards(OptionalAuthGuard)
@Post() @Post()
async sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> { sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
if (mailInfo.listing) { if (mailInfo.listing) {
return await this.mailService.sendInquiry(mailInfo); return this.mailService.sendInquiry(mailInfo);
} else { } else {
return await this.mailService.sendRequest(mailInfo); return this.mailService.sendRequest(mailInfo);
} }
} }
@Post('verify-email')
async sendVerificationEmail(@Body() data: {
email: string,
redirectConfig: {
protocol: string,
hostname: string,
port?: number
}
}): Promise<void | ErrorResponse> {
return await this.mailService.sendVerificationEmail(data.email, data.redirectConfig);
}
@UseGuards(OptionalAuthGuard)
@Post('subscriptionConfirmation')
async sendSubscriptionConfirmation(@Body() user: User): Promise<void | ErrorResponse> {
return await this.mailService.sendSubscriptionConfirmation(user);
}
@UseGuards(OptionalAuthGuard)
@Post('send2Friend')
async send2Friend(@Body() shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
return await this.mailService.send2Friend(shareByEMail);
}
} }

View File

@ -1,32 +1,33 @@
import { MailerModule } from '@nestjs-modules/mailer'; import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { join } from 'path'; import path, { join } from 'path';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module'; import { fileURLToPath } from 'url';
import { DrizzleModule } from '../drizzle/drizzle.module'; import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service.js';
import { GeoModule } from '../geo/geo.module'; import { GeoModule } from '../geo/geo.module.js';
import { GeoService } from '../geo/geo.service'; import { GeoService } from '../geo/geo.service.js';
import { UserModule } from '../user/user.module'; import { UserModule } from '../user/user.module.js';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service.js';
import { MailController } from './mail.controller'; import { MailController } from './mail.controller.js';
import { MailService } from './mail.service'; import { MailService } from './mail.service.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const user = process.env.amazon_user;
const password = process.env.amazon_password;
@Module({ @Module({
imports: [ imports: [
DrizzleModule, DrizzleModule,
UserModule, UserModule,
GeoModule, GeoModule,
FirebaseAdminModule, MailerModule.forRoot({
MailerModule.forRootAsync({
useFactory: () => ({
transport: { transport: {
host: 'email-smtp.us-east-2.amazonaws.com', host: 'email-smtp.us-east-2.amazonaws.com',
secure: false, secure: false,
port: 587, port: 587,
auth: { auth: {
user: process.env.AMAZON_USER, user: 'AKIAU6GDWVAQ2QNFLNWN',
pass: process.env.AMAZON_PASSWORD, pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
}, },
}, },
defaults: { defaults: {
@ -34,17 +35,12 @@ import { MailService } from './mail.service';
}, },
template: { template: {
dir: join(__dirname, 'templates'), dir: join(__dirname, 'templates'),
adapter: new HandlebarsAdapter({ adapter: new HandlebarsAdapter(), // or new PugAdapter() or new EjsAdapter()
eq: function (a, b) {
return a === b;
},
}),
options: { options: {
strict: true, strict: true,
}, },
}, },
}), }),
}),
], ],
providers: [MailService, UserService, FileService, GeoService], providers: [MailService, UserService, FileService, GeoService],
controllers: [MailController], controllers: [MailController],

View File

@ -1,13 +1,13 @@
import { MailerService } from '@nestjs-modules/mailer'; import { MailerService } from '@nestjs-modules/mailer';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { getAuth } from 'firebase-admin/auth'; import path, { join } from 'path';
import { join } from 'path'; import { fileURLToPath } from 'url';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { SenderSchema, ShareByEMail, ShareByEMailSchema, User } from '../models/db.model'; import { SenderSchema } from '../models/db.model.js';
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model'; import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service.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 MailService { export class MailService {
@ -18,7 +18,7 @@ export class MailService {
async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> { async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
try { try {
SenderSchema.parse(mailInfo.sender); const validatedSender = SenderSchema.parse(mailInfo.sender);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const formattedErrors = error.errors.map(err => ({
@ -53,68 +53,9 @@ export class MailService {
}, },
}); });
} }
async sendVerificationEmail(
email: string,
redirectConfig: { protocol: string, hostname: string, port?: number }
): Promise<void | ErrorResponse> {
try {
// Firebase Auth-Instanz holen
const auth = getAuth();
// Baue den Redirect-URL aus den übergebenen Parametern
let continueUrl = `${redirectConfig.protocol}://${redirectConfig.hostname}`;
if (redirectConfig.port) {
continueUrl += `:${redirectConfig.port}`;
}
continueUrl += '/auth/verify-email-success'; // Beispiel für einen Weiterleitungspfad
// Custom Verification Link generieren
const firebaseActionLink = await auth.generateEmailVerificationLink(email, {
url: continueUrl,
handleCodeInApp: false,
});
// Extrahiere den oobCode aus dem Firebase Link
const actionLinkUrl = new URL(firebaseActionLink);
const oobCode = actionLinkUrl.searchParams.get('oobCode');
if (!oobCode) {
throw new BadRequestException('Failed to generate verification code');
}
// Erstelle die benutzerdefinierte URL mit dem oobCode
let customActionLink = `${redirectConfig.protocol}://${redirectConfig.hostname}`;
if (redirectConfig.port) {
customActionLink += `:${redirectConfig.port}`;
}
// Ersetze die Platzhalter mit den tatsächlichen Werten
customActionLink += `/email-authorized?email=${encodeURIComponent(email)}&mode=verifyEmail&oobCode=${oobCode}`;
// Zufallszahl für die E-Mail generieren
const randomNumber = Math.floor(Math.random() * 10000);
// E-Mail senden
await this.mailerService.sendMail({
to: email,
from: '"Bizmatch Team" <info@bizmatch.net>',
subject: 'Verify your email address',
template: join(__dirname, '../..', 'mail/templates/email-verification.hbs'),
context: {
actionLink: customActionLink,
randomNumber: randomNumber
},
});
return;
} catch (error) {
console.error('Error sending verification email:', error);
throw new BadRequestException('Failed to send verification email');
}
}
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> { async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
try { try {
SenderSchema.parse(mailInfo.sender); const validatedSender = SenderSchema.parse(mailInfo.sender);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const formattedErrors = error.errors.map(err => ({
@ -140,48 +81,4 @@ export class MailService {
}, },
}); });
} }
async sendSubscriptionConfirmation(user: User): Promise<void> {
await this.mailerService.sendMail({
to: user.email,
from: `"Bizmatch Support Team" <info@bizmatch.net>`,
subject: `Subscription Confirmation`,
//template: './inquiry', // `.hbs` extension is appended automatically
template: join(__dirname, '../..', 'mail/templates/subscriptionConfirmation.hbs'),
context: {
// ✏️ filling curly brackets with content
firstname: user.firstname,
lastname: user.lastname,
subscriptionPlan: user.subscriptionPlan,
},
});
}
async send2Friend(shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
try {
ShareByEMailSchema.parse(shareByEMail);
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
}));
throw new BadRequestException(formattedErrors);
}
throw error;
}
await this.mailerService.sendMail({
to: shareByEMail.recipientEmail,
from: `"Bizmatch.net" <info@bizmatch.net>`,
subject: `${shareByEMail.type === 'business' ? 'Business' : 'Commercial Property'} For Sale: ${shareByEMail.listingTitle}`,
//template: './inquiry', // `.hbs` extension is appended automatically
template: join(__dirname, '../..', 'mail/templates/send2Friend.hbs'),
context: {
name: shareByEMail.yourName,
email: shareByEMail.yourEmail,
listingTitle: shareByEMail.listingTitle,
url: shareByEMail.url,
id: shareByEMail.id,
type: shareByEMail.type,
},
});
}
} }

View File

@ -1,249 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"> <!-- utf-8 works for most cases -->
<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
<meta name="x-apple-disable-message-reformatting"> <!-- Disable auto-scale in iOS 10 Mail entirely -->
<title>Email address verification</title> <!-- The title tag shows in email notifications, like Android 4.4. -->
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700" rel="stylesheet">
<!-- CSS Reset : BEGIN -->
<style>
/* What it does: Remove spaces around the email design added by some email clients. */
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
html,
body {
margin: 0 auto !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
background: #f1f1f1;
overflow: hidden;
}
/* What it does: Stops email clients resizing small text. */
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
box-sizing: border-box;
}
/* What it does: Centers email on Android 4.4 */
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
/* What it does: Stops Outlook from adding extra spacing to tables. */
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
/* What it does: Fixes webkit padding issue. */
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
/* What it does: Uses a better rendering method when resizing images in IE. */
img {
-ms-interpolation-mode: bicubic;
}
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
a {
text-decoration: none;
}
/* What it does: A work-around for email clients meddling in triggered links. */
*[x-apple-data-detectors],
/* iOS */
.unstyle-auto-detected-links *,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
.a6S {
display: none !important;
opacity: 0.01 !important;
}
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
.im {
color: inherit !important;
}
/* If the above doesn't work, add a .g-img class to any image in question. */
img.g-img+div {
display: none !important;
}
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
/* Create one of these media queries for each additional viewport size you'd like to fix */
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!-- CSS Reset : END -->
<!-- Progressive Enhancements : BEGIN -->
<style>
.primary {
background: #30e3ca;
}
.bg_white {
background: #ffffff;
}
.bg_light {
background: #fafafa;
}
.bg_black {
background: #000000;
}
.bg_dark {
background: rgba(0, 0, 0, .8);
}
.email-section {
padding: 2.5em;
}
body {
font-family: 'Lato', sans-serif;
font-weight: 400;
font-size: 15px;
line-height: 1.8;
color: rgba(0, 0, 0, .4);
}
/*HERO*/
.hero {
position: relative;
z-index: 0;
}
.hero .text {
color: rgba(0, 0, 0, .3);
}
.hero .text h2 {
color: #000;
font-size: 40px;
margin-bottom: 0;
font-weight: 400;
line-height: 1.4;
}
.hero .text h3 {
font-size: 24px;
font-weight: 300;
}
.hero .text h2 span {
font-weight: 600;
color: #30e3ca;
}
.email-body {
display: block;
color: black;
line-height: 32px;
font-weight: 300;
font-family: -apple-system, system-ui, BlinkMacSystemFont, sans-serif;
font-size: 22px;
}
@media (max-width:400px) {
.hero img {
width: 200px !important;
}
}
</style>
</head>
<body width="100%"
style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f1f1f1; display: flex; align-items: center; justify-content: center;">
<div style="width: 100%; background-color: #f1f1f1;">
<div
style="display: none; font-size: 1px;max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
Hello, click on the button below to verify your email address
</div>
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!-- BEGIN BODY -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td valign="middle" class="hero bg_white" style="padding: 3em 0 2em 0;">
<img src="https://github.com/ColorlibHQ/email-templates/blob/master/10/images/email.png?raw=true"
alt="" class="g-img" style="width: 200px; height: auto; margin: auto; display: block;">
</td>
</tr>
<!-- end tr -->
<tr>
<td valign="middle" class="hero bg_white" style="padding: 2em 0 4em 0;">
<table>
<tr>
<td>
<div class="text" style="padding: 0 2.5em; text-align: center;">
<h2 style="margin-bottom: 20px; font-size: 32px;">Verify your email address</h2>
<p class="email-body">
Thanks for signup with us. Click on the button below to verify your email
address.
</p>
<a href="{{actionLink}}" target="_blank"
style="padding:15px 40px; background-color: #5D91E8; color: white;">Verify
your email</a>
<p class="email-body">
If this email wasn't intended for you feel free to delete it.<br />
</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- end tr -->
<span style="color: #f1f1f1; display: none;">{{randomNumber}}</span>
</tr>
</table>
</div>
</div>
</body>
</html>

View File

@ -1,73 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notification</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333333;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
border-bottom: 1px solid #dddddd;
padding-bottom: 20px;
}
.header h1 {
font-size: 24px;
color: #333333;
}
.content {
margin-top: 20px;
}
.content p {
font-size: 16px;
line-height: 1.6;
}
.content .plan-info {
font-weight: bold;
color: #0056b3;
}
.footer {
margin-top: 30px;
text-align: center;
font-size: 14px;
color: #888888;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h1>Notification</h1>
</div>
<div class="content">
<p>Hello,</p>
<p>Your friend {{name}} ({{email}}) believed you might find this <b>{{#if (eq type "commercialProperty")}}Commercial Property{{else if (eq type "business")}}Business{{/if}} for sale listing </b> on <a href="{{url}}">bizmatch.net</a> interesting.</p>
<span class="info-value"><a href="{{url}}/listing/{{id}}">{{listingTitle}}</a></span>
<p>Bizmatch is one of the most reliable platforms for buying and selling businesses.</p>
<p>Best regards,</p>
<p>The Bizmatch Support Team</p>
</div>
<div class="footer">
<p>© 2024 Bizmatch. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@ -1,77 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Subscription Confirmation</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333333;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
border-bottom: 1px solid #dddddd;
padding-bottom: 20px;
}
.header h1 {
font-size: 24px;
color: #333333;
}
.content {
margin-top: 20px;
}
.content p {
font-size: 16px;
line-height: 1.6;
}
.content .plan-info {
font-weight: bold;
color: #0056b3;
}
.footer {
margin-top: 30px;
text-align: center;
font-size: 14px;
color: #888888;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h1>Subscription Confirmation</h1>
</div>
<div class="content">
<p>Dear {{firstname}} {{lastname}},</p>
<p>Thank you for subscribing to our service! We are thrilled to have you on board.</p>
<p>Your subscription details are as follows:</p>
<p><span class="plan-info">{{#if (eq subscriptionPlan "professional")}}Professional Plan (CPA, Attorney, Title Company, Surveyor, Appraiser){{else if (eq subscriptionPlan "broker")}}Business Broker Plan{{/if}}</span></p>
<p>If you have any questions or need further assistance, please feel free to contact our support team at any time.</p>
<p>Thank you for choosing Bizmatch!</p>
<p>Best regards,</p>
<p>The Bizmatch Support Team</p>
</div>
<div class="footer">
<p>© 2024 Bizmatch. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@ -1,24 +1,18 @@
import { LoggerService } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import express from 'express'; import express from 'express';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { AppModule } from './app.module.js';
import { AppModule } from './app.module';
async function bootstrap() { async function bootstrap() {
const server = express(); const server = express();
server.set('trust proxy', true);
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
// const logger = app.get<Logger>(WINSTON_MODULE_NEST_PROVIDER);
const logger = app.get<LoggerService>(WINSTON_MODULE_NEST_PROVIDER);
app.useLogger(logger);
//app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
app.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, Authorization, x-hide-loading', allowedHeaders: 'Content-Type, Accept, Authorization',
}); });
//origin: 'http://localhost:4200',
await app.listen(3000); await app.listen(3000);
} }
bootstrap(); bootstrap();

View File

@ -17,25 +17,20 @@ export interface UserData {
hasCompanyLogo?: boolean; hasCompanyLogo?: boolean;
licensedIn?: string[]; licensedIn?: string[];
gender?: 'male' | 'female'; gender?: 'male' | 'female';
customerType?: 'buyer' | 'seller' | 'professional'; customerType?: 'buyer' | 'broker' | 'professional';
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser'; customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
created?: Date; created?: Date;
updated?: Date; updated?: Date;
} }
export type SortByOptions = 'priceAsc' | 'priceDesc' | 'creationDateFirst' | 'creationDateLast' | 'nameAsc' | 'nameDesc' | 'srAsc' | 'srDesc' | 'cfAsc' | 'cfDesc';
export type SortByTypes = 'professional' | 'listing' | 'business' | 'commercial';
export type Gender = 'male' | 'female'; export type Gender = 'male' | 'female';
export type CustomerType = 'buyer' | 'seller' | 'professional'; export type CustomerType = 'buyer' | 'professional';
export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser'; export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
export type ListingsCategory = 'commercialProperty' | 'business'; export type ListingsCategory = 'commercialProperty' | 'business';
export const GenderEnum = z.enum(['male', 'female']); export const GenderEnum = z.enum(['male', 'female']);
export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']); export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']);
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']); export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']); export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
export const ZodEventTypeEnum = z.enum(['view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact', 'favorite', 'emailus', 'pricing']);
export type EventTypeEnum = z.infer<typeof ZodEventTypeEnum>;
const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']); const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']);
const TypeEnum = z.enum([ const TypeEnum = z.enum([
'automotive', 'automotive',
@ -107,27 +102,16 @@ const USStates = z.enum([
'WY', 'WY',
]); ]);
export const AreasServedSchema = z.object({ export const AreasServedSchema = z.object({
county: z.string().optional().nullable(), county: z.string().nonempty('County is required'),
state: z state: z.string().nonempty('State is required'),
.string()
.nullable()
.refine(val => val !== null && val !== '', {
message: 'State is required',
}),
}); });
export const LicensedInSchema = z.object({ export const LicensedInSchema = z.object({
state: z registerNo: z.string().nonempty('Registration number is required'),
.string() state: z.string().nonempty('State is required'),
.nullable()
.refine(val => val !== null && val !== '', {
message: 'State is required',
}),
registerNo: z.string().nonempty('License number is required'),
}); });
export const GeoSchema = z export const GeoSchema = z.object({
.object({ city: z.string(),
name: z.string().optional().nullable(),
state: z.string().refine(val => USStates.safeParse(val).success, { state: z.string().refine(val => USStates.safeParse(val).success, {
message: 'Invalid state. Must be a valid 2-letter US state code.', message: 'Invalid state. Must be a valid 2-letter US state code.',
}), }),
@ -147,33 +131,21 @@ export const GeoSchema = z
message: 'Longitude muss zwischen -180 und 180 liegen', message: 'Longitude muss zwischen -180 und 180 liegen',
}, },
), ),
county: z.string().optional().nullable(), });
housenumber: z.string().optional().nullable(), const phoneRegex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
street: z.string().optional().nullable(),
zipCode: z.number().optional().nullable(),
})
.superRefine((data, ctx) => {
if (!data.state) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'You need to select at least a state',
path: ['name'],
});
}
});
const phoneRegex = /^(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
export const UserSchema = z export const UserSchema = z
.object({ .object({
id: z.string().uuid().optional().nullable(), id: z.string().uuid().optional().nullable(),
firstname: z.string().min(3, { message: 'First name must contain at least 2 characters' }), firstname: z.string().min(2, { message: 'First name must contain at least 2 characters' }),
lastname: z.string().min(3, { message: 'Last name must contain at least 2 characters' }), lastname: z.string().min(2, { message: 'Last name must contain at least 2 characters' }),
email: z.string().email({ message: 'Invalid email address' }), email: z.string().email({ message: 'Invalid email address' }),
phoneNumber: z.string().optional().nullable(), phoneNumber: z.string().optional().nullable(),
description: z.string().optional().nullable(), description: z.string().optional().nullable(),
companyName: z.string().optional().nullable(), companyName: z.string().optional().nullable(),
companyOverview: z.string().optional().nullable(), companyOverview: z.string().optional().nullable(),
companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(), companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
location: GeoSchema.optional().nullable(), companyLocation: GeoSchema.optional().nullable(),
offeredServices: z.string().optional().nullable(), offeredServices: z.string().optional().nullable(),
areasServed: z.array(AreasServedSchema).optional().nullable(), areasServed: z.array(AreasServedSchema).optional().nullable(),
hasProfile: z.boolean().optional().nullable(), hasProfile: z.boolean().optional().nullable(),
@ -184,9 +156,6 @@ export const UserSchema = z
customerSubType: CustomerSubTypeEnum.optional().nullable(), customerSubType: CustomerSubTypeEnum.optional().nullable(),
created: z.date().optional().nullable(), created: z.date().optional().nullable(),
updated: z.date().optional().nullable(), updated: z.date().optional().nullable(),
subscriptionId: z.string().optional().nullable(),
subscriptionPlan: SubscriptionTypeEnum.optional().nullable(),
showInDirectory: z.boolean(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.customerType === 'professional') { if (data.customerType === 'professional') {
@ -197,13 +166,7 @@ export const UserSchema = z
path: ['customerSubType'], path: ['customerSubType'],
}); });
} }
if (!data.companyName || data.companyName.length < 6) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Company Name must contain at least 6 characters for professional customers',
path: ['companyName'],
});
}
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) { if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
@ -236,11 +199,11 @@ export const UserSchema = z
}); });
} }
if (!data.location) { if (!data.companyLocation) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'Company location is required for professional customers', message: 'Company location is required for professional customers',
path: ['location'], path: ['companyLocation'],
}); });
} }
@ -258,8 +221,7 @@ export type AreasServed = z.infer<typeof AreasServedSchema>;
export type LicensedIn = z.infer<typeof LicensedInSchema>; export type LicensedIn = z.infer<typeof LicensedInSchema>;
export type User = z.infer<typeof UserSchema>; export type User = z.infer<typeof UserSchema>;
export const BusinessListingSchema = z export const BusinessListingSchema = z.object({
.object({
id: z.string().uuid().optional().nullable(), id: z.string().uuid().optional().nullable(),
email: z.string().email(), email: z.string().email(),
type: z.string().refine(val => TypeEnum.safeParse(val).success, { type: z.string().refine(val => TypeEnum.safeParse(val).success, {
@ -268,51 +230,26 @@ export const BusinessListingSchema = z
title: z.string().min(10), title: z.string().min(10),
description: z.string().min(10), description: z.string().min(10),
location: GeoSchema, location: GeoSchema,
price: z.number().positive().optional().nullable(), price: z.number().positive().max(1000000000),
favoritesForUser: z.array(z.string()), favoritesForUser: z.array(z.string()),
draft: z.boolean(), draft: z.boolean(),
listingsCategory: ListingsCategoryEnum, listingsCategory: ListingsCategoryEnum,
realEstateIncluded: z.boolean().optional().nullable(), realEstateIncluded: z.boolean().optional().nullable(),
leasedLocation: z.boolean().optional().nullable(), leasedLocation: z.boolean().optional().nullable(),
franchiseResale: z.boolean().optional().nullable(), franchiseResale: z.boolean().optional().nullable(),
salesRevenue: z.number().positive().nullable(), salesRevenue: z.number().positive().max(100000000),
cashFlow: z.number().optional().nullable(), cashFlow: z.number().positive().max(100000000),
ffe: z.number().optional().nullable(), supportAndTraining: z.string().min(5),
inventory: z.number().optional().nullable(),
supportAndTraining: z.string().min(5).optional().nullable(),
employees: z.number().int().positive().max(100000).optional().nullable(), employees: z.number().int().positive().max(100000).optional().nullable(),
established: z.number().int().min(1).max(250).optional().nullable(), established: z.number().int().min(1800).max(2030).optional().nullable(),
internalListingNumber: z.number().int().positive().optional().nullable(), internalListingNumber: z.number().int().positive().optional().nullable(),
reasonForSale: z.string().min(5).optional().nullable(), reasonForSale: z.string().min(5).optional().nullable(),
brokerLicencing: z.string().optional().nullable(), brokerLicencing: z.string().min(5).optional().nullable(),
internals: z.string().min(5).optional().nullable(), internals: z.string().min(5).optional().nullable(),
imageName: z.string().optional().nullable(), imageName: z.string().optional().nullable(),
created: z.date(), created: z.date(),
updated: z.date(), updated: z.date(),
}) });
.superRefine((data, ctx) => {
if (data.price && data.price > 1000000000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Price must less than or equal $1,000,000,000',
path: ['price'],
});
}
if (data.salesRevenue && data.salesRevenue > 100000000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'SalesRevenue must less than or equal $100,000,000',
path: ['salesRevenue'],
});
}
if (data.cashFlow && data.cashFlow > 100000000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'CashFlow must less than or equal $100,000,000',
path: ['cashFlow'],
});
}
});
export type BusinessListing = z.infer<typeof BusinessListingSchema>; export type BusinessListing = z.infer<typeof BusinessListingSchema>;
export const CommercialPropertyListingSchema = z export const CommercialPropertyListingSchema = z
@ -326,25 +263,16 @@ export const CommercialPropertyListingSchema = z
title: z.string().min(10), title: z.string().min(10),
description: z.string().min(10), description: z.string().min(10),
location: GeoSchema, location: GeoSchema,
price: z.number().positive().optional().nullable(), price: z.number().positive().max(1000000000),
favoritesForUser: z.array(z.string()), favoritesForUser: z.array(z.string()),
listingsCategory: ListingsCategoryEnum, listingsCategory: ListingsCategoryEnum,
internalListingNumber: z.number().int().positive().optional().nullable(),
draft: z.boolean(), draft: z.boolean(),
imageOrder: z.array(z.string()), imageOrder: z.array(z.string()),
imagePath: z.string().nullable().optional(), imagePath: z.string().nullable().optional(),
created: z.date(), created: z.date(),
updated: z.date(), updated: z.date(),
}) })
.superRefine((data, ctx) => { .strict();
if (data.price && data.price > 1000000000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Price must less than or equal $1,000,000,000',
path: ['price'],
});
}
});
export type CommercialPropertyListing = z.infer<typeof CommercialPropertyListingSchema>; export type CommercialPropertyListing = z.infer<typeof CommercialPropertyListingSchema>;
@ -360,30 +288,3 @@ export const SenderSchema = z.object({
comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }), comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }),
}); });
export type Sender = z.infer<typeof SenderSchema>; export type Sender = z.infer<typeof SenderSchema>;
export const ShareByEMailSchema = z.object({
yourName: z.string().min(6, { message: 'Name must be at least 6 characters long' }),
recipientEmail: z.string().email({ message: 'Invalid email address' }),
yourEmail: z.string().email({ message: 'Invalid email address' }),
listingTitle: z.string().optional().nullable(),
url: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
id: z.string().optional().nullable(),
type: ListingsCategoryEnum,
});
export type ShareByEMail = z.infer<typeof ShareByEMailSchema>;
export const ListingEventSchema = z.object({
id: z.string().uuid(), // UUID für das Event
listingId: z.string().uuid().optional().nullable(), // UUID für das Listing
email: z.string().email().optional().nullable(), // EMail des den Benutzer, optional, wenn kein Benutzer eingeloggt ist
eventType: ZodEventTypeEnum, // Die Event-Typen
eventTimestamp: z.string().datetime().or(z.date()), // Der Zeitstempel des Events, kann ein String im ISO-Format oder ein Date-Objekt sein
userIp: z.string().max(45).optional().nullable(), // IP-Adresse des Benutzers, optional
userAgent: z.string().max(255).optional().nullable(), // User-Agent des Benutzers, optional
locationCountry: z.string().max(100).optional().nullable(), // Land, optional
locationCity: z.string().max(100).optional().nullable(), // Stadt, optional
locationLat: z.string().max(20).optional().nullable(), // Latitude, als String
locationLng: z.string().max(20).optional().nullable(), // Longitude, als String
referrer: z.string().max(255).optional().nullable(), // Referrer URL, optional
additionalData: z.record(z.any()).optional().nullable(), // JSON für zusätzliche Daten, z.B. soziale Medien, optional
});
export type ListingEvent = z.infer<typeof ListingEventSchema>;

View File

@ -1,6 +1,4 @@
import Stripe from 'stripe'; import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
import { BusinessListing, CommercialPropertyListing, Sender, SortByOptions, SortByTypes, User } from './db.model';
import { State } from './server.model';
export interface StatesResult { export interface StatesResult {
state: string; state: string;
@ -11,12 +9,6 @@ export interface KeyValue {
name: string; name: string;
value: string; value: string;
} }
export interface KeyValueAsSortBy {
name: string;
value: SortByOptions;
type?: SortByTypes;
selectName?: string;
}
export interface KeyValueRatio { export interface KeyValueRatio {
label: string; label: string;
value: number; value: number;
@ -67,13 +59,12 @@ export interface ListCriteria {
page: number; page: number;
types: string[]; types: string[];
state: string; state: string;
city: GeoResult; city: string;
prompt: string; prompt: string;
searchType: 'exact' | 'radius'; searchType: 'exact' | 'radius';
// radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'; // radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500';
radius: number; radius: number;
criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
sortBy?: SortByOptions;
} }
export interface BusinessListingCriteria extends ListCriteria { export interface BusinessListingCriteria extends ListCriteria {
minPrice: number; minPrice: number;
@ -84,13 +75,13 @@ export interface BusinessListingCriteria extends ListCriteria {
maxCashFlow: number; maxCashFlow: number;
minNumberEmployees: number; minNumberEmployees: number;
maxNumberEmployees: number; maxNumberEmployees: number;
establishedMin: number; establishedSince: number;
establishedUntil: number;
realEstateChecked: boolean; realEstateChecked: boolean;
leasedLocation: boolean; leasedLocation: boolean;
franchiseResale: boolean; franchiseResale: boolean;
title: string; title: string;
brokerName: string; brokerName: string;
email: string;
criteriaType: 'businessListings'; criteriaType: 'businessListings';
} }
export interface CommercialPropertyListingCriteria extends ListCriteria { export interface CommercialPropertyListingCriteria extends ListCriteria {
@ -100,7 +91,8 @@ export interface CommercialPropertyListingCriteria extends ListCriteria {
criteriaType: 'commercialPropertyListings'; criteriaType: 'commercialPropertyListings';
} }
export interface UserListingCriteria extends ListCriteria { export interface UserListingCriteria extends ListCriteria {
brokerName: string; firstname: string;
lastname: string;
companyName: string; companyName: string;
counties: string[]; counties: string[];
criteriaType: 'brokerListings'; criteriaType: 'brokerListings';
@ -120,16 +112,13 @@ export interface KeycloakUser {
requiredActions?: any[]; requiredActions?: any[];
notBefore?: number; notBefore?: number;
access?: Access; access?: Access;
attributes?: Attributes;
} }
export interface JwtUser { export interface JwtUser {
email: string; userId: string;
role: string; username: string;
uid: string; firstname: string;
} lastname: string;
interface Attributes { roles: string[];
[key: string]: any;
priceID: any;
} }
export interface Access { export interface Access {
manageGroupMembership: boolean; manageGroupMembership: boolean;
@ -177,7 +166,6 @@ export interface JwtToken {
family_name: string; family_name: string;
email: string; email: string;
user_id: string; user_id: string;
price_id: string;
} }
export interface JwtPayload { export interface JwtPayload {
sub: string; sub: string;
@ -236,66 +224,24 @@ export interface UploadParams {
} }
export interface GeoResult { export interface GeoResult {
id: number; id: number;
name: string; city: string;
street?: string;
housenumber?: string;
county?: string;
zipCode?: number;
state: string; state: string;
// state_code: string;
latitude: number; latitude: number;
longitude: number; longitude: number;
} }
interface CityResult { export interface CityAndStateResult {
id: number; id: number;
type: 'city'; name: string;
content: GeoResult; type: string;
state: string;
} }
interface StateResult {
id: number;
type: 'state';
content: State;
}
export type CityAndStateResult = CityResult | StateResult;
export interface CountyResult { export interface CountyResult {
id: number; id: number;
name: string; name: string;
state: string; state: string;
state_code: string; state_code: string;
} }
export interface LogMessage {
severity: 'error' | 'info';
text: string;
}
export interface ModalResult {
accepted: boolean;
criteria?: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
}
export interface Checkout {
priceId: string;
email: string;
name: string;
}
export type UserRole = 'admin' | 'pro' | 'guest' | null;
export interface FirebaseUserInfo {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
phoneNumber: string | null;
disabled: boolean;
emailVerified: boolean;
role: UserRole;
creationTime?: string;
lastSignInTime?: string;
customClaims?: Record<string, any>;
}
export interface UsersResponse {
users: FirebaseUserInfo[];
totalCount: number;
pageToken?: string;
}
export function isEmpty(value: any): boolean { export function isEmpty(value: any): boolean {
// Check for undefined or null // Check for undefined or null
if (value === undefined || value === null) { if (value === undefined || value === null) {
@ -335,7 +281,7 @@ export interface ValidationMessage {
field: string; field: string;
message: string; message: string;
} }
export function createDefaultUser(email: string, firstname: string, lastname: string, subscriptionPlan: 'professional' | 'broker'): User { export function createDefaultUser(email: string, firstname: string, lastname: string): User {
return { return {
id: undefined, id: undefined,
email, email,
@ -346,7 +292,7 @@ export function createDefaultUser(email: string, firstname: string, lastname: st
companyName: null, companyName: null,
companyOverview: null, companyOverview: null,
companyWebsite: null, companyWebsite: null,
location: null, companyLocation: null,
offeredServices: null, offeredServices: null,
areasServed: [], areasServed: [],
hasProfile: false, hasProfile: false,
@ -357,8 +303,6 @@ export function createDefaultUser(email: string, firstname: string, lastname: st
customerSubType: null, customerSubType: null,
created: new Date(), created: new Date(),
updated: new Date(), updated: new Date(),
subscriptionId: null,
subscriptionPlan: subscriptionPlan,
}; };
} }
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing { export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
@ -408,25 +352,3 @@ export function createDefaultBusinessListing(): BusinessListing {
listingsCategory: 'business', listingsCategory: 'business',
}; };
} }
export type StripeSubscription = Stripe.Subscription;
export type StripeUser = Stripe.Customer;
export type IpInfo = {
ip: string;
city: string;
region: string;
country: string;
loc: string; // Coordinates in "latitude,longitude" format
org: string;
postal: string;
timezone: string;
};
export interface CombinedUser {
keycloakUser?: KeycloakUser;
appUser?: User;
stripeUser?: StripeUser;
stripeSubscription?: StripeSubscription;
}
export interface RealIpInfo {
ip: string;
countryCode?: string;
}

View File

@ -70,34 +70,3 @@ export interface CountyRequest {
prefix: string; prefix: string;
states: string[]; states: string[];
} }
export interface Address {
house_number: string;
road: string;
quarter: string;
suburb: string;
city: string;
county: string;
state: string;
ISO3166_2_lvl4: string;
postcode: string;
country: string;
country_code: string;
}
export interface Place {
place_id: number;
licence: string;
osm_type: string;
osm_id: number;
lat: string;
lon: string;
class: string;
type: string;
place_rank: number;
importance: number;
addresstype: string;
name: string;
display_name: string;
address: Address;
boundingbox: [string, string, string, string];
}

View File

@ -1,42 +1,25 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { ClsService } from 'nestjs-cls';
import { getRealIpInfo } from 'src/utils/ip.util';
@Injectable() @Injectable()
export class RequestDurationMiddleware implements NestMiddleware { export class RequestDurationMiddleware implements NestMiddleware {
private readonly logger = new Logger(RequestDurationMiddleware.name); private readonly logger = new Logger(RequestDurationMiddleware.name);
constructor(private readonly cls: ClsService) {}
use(req: Request, res: Response, next: NextFunction) { use(req: Request, res: Response, next: NextFunction) {
const { ip, countryCode } = getRealIpInfo(req); 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`;
// Setze die IP-Adresse und den Ländercode im CLS-Kontext if (req.method === 'POST' || req.method === 'PUT') {
try { const body = JSON.stringify(req.body);
this.cls.set('ip', ip); logMessage += ` - Body: ${body}`;
this.cls.set('countryCode', countryCode);
} catch (error) {
this.logger.error('Failed to set CLS context', error);
} }
// const start = Date.now(); this.logger.log(logMessage);
});
// this.logger.log(`Entering ${req.method} ${req.originalUrl} from ${ip}`);
// res.on('finish', () => {
// const duration = Date.now() - start;
// const userEmail = this.cls.get('userEmail') || 'unknown';
// let logMessage = `${req.method} ${req.originalUrl} - ${duration}ms - IP: ${ip} - User: ${userEmail}`;
// if (req.method === 'POST' || req.method === 'PUT') {
// const body = JSON.stringify(req.body);
// logMessage += ` - Incoming Body: ${body}`;
// }
// this.logger.log(logMessage);
// });
next(); next();
} }
} }

View File

@ -1,12 +1,9 @@
import { Controller, Get, UseGuards } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard'; import { SelectOptionsService } from './select-options.service.js';
import { SelectOptionsService } from './select-options.service';
@Controller('select-options') @Controller('select-options')
export class SelectOptionsController { export class SelectOptionsController {
constructor(private selectOptionsService: SelectOptionsService) {} constructor(private selectOptionsService: SelectOptionsService) {}
@UseGuards(OptionalAuthGuard)
@Get() @Get()
getSelectOption(): any { getSelectOption(): any {
return { return {
@ -18,7 +15,6 @@ export class SelectOptionsController {
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty, typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
customerSubTypes: this.selectOptionsService.customerSubTypes, customerSubTypes: this.selectOptionsService.customerSubTypes,
distances: this.selectOptionsService.distances, distances: this.selectOptionsService.distances,
sortByOptions: this.selectOptionsService.sortByOptions,
}; };
} }
} }

View File

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

View File

@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ImageType, KeyValue, KeyValueAsSortBy, KeyValueStyle } from '../models/main.model'; 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: 'automotive', oldValue: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-500' }, { name: 'Automotive', value: 'automotive', oldValue: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
{ name: 'Industrial Services', value: 'industrialServices', oldValue: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' }, { name: 'Industrial Services', value: 'industrialServices', oldValue: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
{ name: 'Food and Restaurant', value: 'foodAndRestaurant', oldValue: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' }, { name: 'Food and Restaurant', value: 'foodAndRestaurant', oldValue: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
{ name: 'Real Estate', value: 'realEstate', oldValue: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' }, { name: 'Real Estate', value: 'realEstate', oldValue: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
@ -35,19 +35,7 @@ export class SelectOptionsService {
{ name: '$1M', value: '1000000' }, { name: '$1M', value: '1000000' },
{ name: '$5M', value: '5000000' }, { name: '$5M', value: '5000000' },
]; ];
public sortByOptions: Array<KeyValueAsSortBy> = [
{ name: 'Price Asc', value: 'priceAsc', type: 'listing' },
{ name: 'Price Desc', value: 'priceDesc', type: 'listing' },
{ name: 'Sales Revenue Asc', value: 'srAsc', type: 'business' },
{ name: 'Sales Revenue Desc', value: 'srDesc', type: 'business' },
{ name: 'Cash Flow Asc', value: 'cfAsc', type: 'business' },
{ name: 'Cash Flow Desc', value: 'cfDesc', type: 'business' },
{ name: 'Creation Date First', value: 'creationDateFirst', type: 'listing' },
{ name: 'Creation Date Last', value: 'creationDateLast', type: 'listing' },
{ name: 'Name Asc', value: 'nameAsc', type: 'professional' },
{ name: 'Name Desc', value: 'nameDesc', type: 'professional' },
{ name: 'Sort', value: null, selectName: 'Default Sorting' },
];
public distances: Array<KeyValue> = [ public distances: Array<KeyValue> = [
{ name: '5 miles', value: '5' }, { name: '5 miles', value: '5' },
{ name: '20 miles', value: '20' }, { name: '20 miles', value: '20' },
@ -64,7 +52,6 @@ export class SelectOptionsService {
]; ];
public customerTypes: Array<KeyValue> = [ public customerTypes: Array<KeyValue> = [
{ name: 'Buyer', value: 'buyer' }, { name: 'Buyer', value: 'buyer' },
{ name: 'Commercial Property Seller', value: 'seller' },
{ name: 'Professional', value: 'professional' }, { name: 'Professional', value: 'professional' },
]; ];
public customerSubTypes: Array<KeyValue> = [ public customerSubTypes: Array<KeyValue> = [

View File

@ -1,27 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { AuthService } from './auth/auth.service';
@Injectable()
@Command({ name: 'setup-admin', description: 'Set up the first admin user' })
export class SetupAdminCommand extends CommandRunner {
constructor(private readonly authService: AuthService) {
super();
}
async run(passedParams: string[]): Promise<void> {
if (passedParams.length < 1) {
console.error('Please provide a user UID');
return;
}
const uid = passedParams[0];
try {
await this.authService.setUserRole(uid, 'admin');
console.log(`User ${uid} has been set as admin`);
} catch (error) {
console.error('Error setting admin role:', error);
}
}
}

View File

@ -1,15 +1,11 @@
import { BadRequestException, Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ZodError } from 'zod'; import { FileService } from '../file/file.service.js';
import { FileService } from '../file/file.service'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
import { AuthGuard } from 'src/jwt-auth/auth.guard';
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
import { User } from '../models/db.model'; import { User } from '../models/db.model';
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model'; import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model.js';
import { UserService } from './user.service'; import { UserService } from './user.service.js';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
@ -18,70 +14,52 @@ export class UserController {
private fileService: FileService, private fileService: FileService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@UseGuards(OptionalJwtAuthGuard)
@UseGuards(OptionalAuthGuard)
@Get() @Get()
async findByMail(@Request() req, @Query('mail') mail: string): Promise<User> { findByMail(@Request() req, @Query('mail') mail: string): any {
const user = await this.userService.getUserByMail(mail, req.user as JwtUser); this.logger.info(`Searching for user with EMail: ${mail}`);
const user = this.userService.getUserByMail(mail, req.user as JwtUser);
this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user; return user;
} }
@UseGuards(OptionalAuthGuard)
@Get(':id') @Get(':id')
async findById(@Param('id') id: string): Promise<User> { findById(@Param('id') id: string): any {
const user = await this.userService.getUserById(id); 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; return user;
} }
@UseGuards(AdminGuard)
@Get('user/all')
async getAllUser(): Promise<User[]> {
return await this.userService.getAllUser();
}
@UseGuards(OptionalAuthGuard)
@Post() @Post()
async save(@Body() user: any): Promise<User> { save(@Body() user: any): Promise<User> {
try { this.logger.info(`Saving user: ${JSON.stringify(user)}`);
const savedUser = await this.userService.saveUser(user); const savedUser = this.userService.saveUser(user);
return savedUser; this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);
} catch (error) {
if (error instanceof ZodError) {
const filteredErrors = error.errors
.map(item => ({
...item,
field: item.path[0],
}))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
}
throw error; // Andere Fehler einfach durchreichen
}
}
@UseGuards(OptionalAuthGuard)
@Post('guaranteed')
async saveGuaranteed(@Body() user: any): Promise<User> {
const savedUser = await this.userService.saveUser(user, false);
return savedUser; return savedUser;
} }
@UseGuards(OptionalAuthGuard)
@Post('search') @Post('search')
async find(@Body() criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> { find(@Body() criteria: UserListingCriteria): any {
const foundUsers = await this.userService.searchUserListings(criteria); this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
const foundUsers = this.userService.searchUserListings(criteria);
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
return foundUsers; return foundUsers;
} }
@UseGuards(OptionalAuthGuard)
@Post('findTotal') @Post('findTotal')
async findTotal(@Body() criteria: UserListingCriteria): Promise<number> { findTotal(@Body() criteria: UserListingCriteria): Promise<number> {
return await this.userService.getUserListingsCount(criteria); return this.userService.getUserListingsCount(criteria);
}
@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;
} }
@UseGuards(AuthGuard)
@Get('subscriptions/:id') @Get('subscriptions/:id')
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> { async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
const subscriptions = []; const subscriptions = this.fileService.getSubscriptions();
const user = await this.userService.getUserById(id); const user = await this.userService.getUserById(id);
subscriptions.forEach(s => { subscriptions.forEach(s => {
s.userId = user.id; s.userId = user.id;

View File

@ -1,14 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module'; import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { DrizzleModule } from '../drizzle/drizzle.module'; import { FileService } from '../file/file.service.js';
import { FileService } from '../file/file.service'; import { GeoModule } from '../geo/geo.module.js';
import { GeoModule } from '../geo/geo.module'; import { GeoService } from '../geo/geo.service.js';
import { GeoService } from '../geo/geo.service'; import { UserController } from './user.controller.js';
import { UserController } from './user.controller'; import { UserService } from './user.service.js';
import { UserService } from './user.service';
@Module({ @Module({
imports: [DrizzleModule, GeoModule,FirebaseAdminModule], imports: [DrizzleModule, GeoModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService, FileService, GeoService], providers: [UserService, FileService, GeoService],
}) })

View File

@ -1,15 +1,16 @@
import { Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { and, asc, count, desc, eq, inArray, or, SQL, sql } from 'drizzle-orm'; import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver'; import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as schema from '../drizzle/schema'; import { ZodError } from 'zod';
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema'; import * as schema from '../drizzle/schema.js';
import { FileService } from '../file/file.service'; import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js';
import { GeoService } from '../geo/geo.service'; import { FileService } from '../file/file.service.js';
import { User, UserSchema } from '../models/db.model'; import { GeoService } from '../geo/geo.service.js';
import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model'; import { User, UserSchema } from '../models/db.model.js';
import { getDistanceQuery, splitName } from '../utils'; import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
import { convertDrizzleUserToUser, convertUserToDrizzleUser, getDistanceQuery } from '../utils.js';
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number]; type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
@Injectable() @Injectable()
@ -23,68 +24,56 @@ export class UserService {
private getWhereConditions(criteria: UserListingCriteria): SQL[] { private getWhereConditions(criteria: UserListingCriteria): SQL[] {
const whereConditions: SQL[] = []; const whereConditions: SQL[] = [];
whereConditions.push(sql`(${schema.users_json.data}->>'customerType') = 'professional'`); whereConditions.push(eq(schema.users.customerType, 'professional'));
if (criteria.city && criteria.searchType === 'exact') { if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(sql`(${schema.users_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`); whereConditions.push(ilike(schema.users.city, `%${criteria.city}%`));
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
whereConditions.push(sql`${getDistanceQuery(schema.users_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
} }
if (criteria.types && criteria.types.length > 0) { if (criteria.types && criteria.types.length > 0) {
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types)); // whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
whereConditions.push(inArray(sql`${schema.users_json.data}->>'customerSubType'`, criteria.types as CustomerSubType[])); whereConditions.push(inArray(schema.users.customerSubType, criteria.types as CustomerSubType[]));
} }
if (criteria.brokerName) { if (criteria.firstname) {
const { firstname, lastname } = splitName(criteria.brokerName); whereConditions.push(ilike(schema.users.firstname, `%${criteria.firstname}%`));
whereConditions.push(sql`(${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`); }
if (criteria.lastname) {
whereConditions.push(ilike(schema.users.lastname, `%${criteria.lastname}%`));
} }
if (criteria.companyName) { if (criteria.companyName) {
whereConditions.push(sql`(${schema.users_json.data}->>'companyName') ILIKE ${`%${criteria.companyName}%`}`); whereConditions.push(ilike(schema.users.companyName, `%${criteria.companyName}%`));
} }
if (criteria.counties && criteria.counties.length > 0) { if (criteria.counties && criteria.counties.length > 0) {
whereConditions.push(or(...criteria.counties.map(county => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users_json.data}->'areasServed') AS area WHERE area->>'county' ILIKE ${`%${county}%`})`))); whereConditions.push(or(...criteria.counties.map(county => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'county' ILIKE ${`%${county}%`})`)));
} }
if (criteria.state) { if (criteria.state) {
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users_json.data}->'areasServed') AS area WHERE area->>'state' = ${criteria.state})`); whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`);
} }
//never show user which denied
whereConditions.push(sql`(${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE`);
return whereConditions; return whereConditions;
} }
async searchUserListings(criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> { async searchUserListings(criteria: UserListingCriteria) {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
const query = this.conn.select().from(schema.users_json); const query = this.conn.select().from(schema.users);
const whereConditions = this.getWhereConditions(criteria); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
const whereClause = and(...whereConditions); const whereClause = and(...whereConditions);
query.where(whereClause); query.where(whereClause);
} }
// Sortierung
switch (criteria.sortBy) {
case 'nameAsc':
query.orderBy(asc(sql`${schema.users_json.data}->>'lastname'`));
break;
case 'nameDesc':
query.orderBy(desc(sql`${schema.users_json.data}->>'lastname'`));
break;
default:
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
break;
}
// Paginierung // Paginierung
query.limit(length).offset(start); query.limit(length).offset(start);
const data = await query; const data = await query;
const results = data.map(u => ({ id: u.id, email: u.email, ...(u.data as User) }) as User); const results = data.map(r => convertDrizzleUserToUser(r));
const totalCount = await this.getUserListingsCount(criteria); const totalCount = await this.getUserListingsCount(criteria);
return { return {
@ -93,7 +82,7 @@ export class UserService {
}; };
} }
async getUserListingsCount(criteria: UserListingCriteria): Promise<number> { async getUserListingsCount(criteria: UserListingCriteria): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(schema.users_json); const countQuery = this.conn.select({ value: count() }).from(schema.users);
const whereConditions = this.getWhereConditions(criteria); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
@ -105,31 +94,33 @@ export class UserService {
return totalCount; return totalCount;
} }
async getUserByMail(email: string, jwtuser?: JwtUser) { async getUserByMail(email: string, jwtuser?: JwtUser) {
const users = await this.conn.select().from(schema.users_json).where(eq(schema.users_json.email, email)); const users = (await this.conn
.select()
.from(schema.users)
.where(sql`email = ${email}`)) as User[];
if (users.length === 0) { if (users.length === 0) {
const user: User = { id: undefined, customerType: 'professional', ...createDefaultUser(email, '', '', null) }; const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname) };
const u = await this.saveUser(user, false); const u = await this.saveUser(user);
return u; return convertDrizzleUserToUser(u);
} else { } else {
const user = { id: users[0].id, email: users[0].email, ...(users[0].data as User) } as User; const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email)); user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user; return convertDrizzleUserToUser(user);
} }
} }
async getUserById(id: string) { async getUserById(id: string) {
const users = await this.conn.select().from(schema.users_json).where(eq(schema.users_json.id, id)); const users = (await this.conn
.select()
.from(schema.users)
.where(sql`id = ${id}`)) as User[];
const user = { id: users[0].id, email: users[0].email, ...(users[0].data as User) } as User; const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email)); user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user; return convertDrizzleUserToUser(user);
} }
async getAllUser() { async saveUser(user: User): Promise<User> {
const users = await this.conn.select().from(schema.users_json);
return users.map(u => ({ id: u.id, email: u.email, ...(u.data as User) }) as User);
}
async saveUser(user: User, processValidation = true): Promise<User> {
try { try {
user.updated = new Date(); user.updated = new Date();
if (user.id) { if (user.id) {
@ -137,22 +128,29 @@ export class UserService {
} else { } else {
user.created = new Date(); user.created = new Date();
} }
let validatedUser = user; const validatedUser = UserSchema.parse(user);
if (processValidation) { const drizzleUser = convertUserToDrizzleUser(validatedUser);
validatedUser = UserSchema.parse(user);
}
//const drizzleUser = convertUserToDrizzleUser(validatedUser);
const { id: _, ...rest } = validatedUser;
const drizzleUser = { email: user.email, data: rest };
if (user.id) { if (user.id) {
const [updateUser] = await this.conn.update(schema.users_json).set(drizzleUser).where(eq(schema.users_json.id, user.id)).returning(); const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning();
return { id: updateUser.id, email: updateUser.email, ...(updateUser.data as User) } as User; return convertDrizzleUserToUser(updateUser) as User;
} else { } else {
const [newUser] = await this.conn.insert(schema.users_json).values(drizzleUser).returning(); const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning();
return { id: newUser.id, email: newUser.email, ...(newUser.data as User) } as User; return convertDrizzleUserToUser(newUser) as User;
} }
} catch (error) { } catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
}));
throw new BadRequestException(formattedErrors);
}
throw error; throw error;
} }
} }
async getStates(): Promise<any[]> {
const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`;
const result = await this.conn.execute(query);
return result.rows;
}
} }

View File

@ -1,5 +1,6 @@
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { businesses, businesses_json, commercials, commercials_json, users, users_json } from './drizzle/schema'; import { businesses, commercials, users } from './drizzle/schema.js';
import { BusinessListing, CommercialPropertyListing, User } from './models/db.model.js';
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
export function convertStringToNullUndefined(value) { export function convertStringToNullUndefined(value) {
@ -16,31 +17,109 @@ export function convertStringToNullUndefined(value) {
return value; return value;
} }
export const getDistanceQuery = (schema: typeof businesses_json | typeof commercials_json | typeof users_json, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => { export const getDistanceQuery = (schema: typeof businesses | typeof commercials | typeof users, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => {
const radius = unit === 'km' ? EARTH_RADIUS_KM : EARTH_RADIUS_MILES; const radius = unit === 'km' ? EARTH_RADIUS_KM : EARTH_RADIUS_MILES;
return sql` return sql`
${radius} * 2 * ASIN(SQRT( ${radius} * 2 * ASIN(SQRT(
POWER(SIN((${lat} - (${schema.data}->'location'->>'latitude')::float) * PI() / 180 / 2), 2) + POWER(SIN((${lat} - ${schema.latitude}) * PI() / 180 / 2), 2) +
COS(${lat} * PI() / 180) * COS((${schema.data}->'location'->>'latitude')::float * PI() / 180) * COS(${lat} * PI() / 180) * COS(${schema.latitude} * PI() / 180) *
POWER(SIN((${lon} - (${schema.data}->'location'->>'longitude')::float) * PI() / 180 / 2), 2) POWER(SIN((${lon} - ${schema.longitude}) * PI() / 180 / 2), 2)
)) ))
`; `;
}; };
export type DrizzleUser = typeof users.$inferSelect; type DrizzleUser = typeof users.$inferSelect;
export type DrizzleBusinessListing = typeof businesses.$inferSelect; type DrizzleBusinessListing = typeof businesses.$inferSelect;
export type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect; type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing {
export function splitName(fullName: string): { firstname: string; lastname: string } { return flattenObject(businessListing);
const parts = fullName.trim().split(/\s+/); // Teile den Namen am Leerzeichen auf }
export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing {
if (parts.length === 1) { const o = {
// Falls es nur ein Teil gibt, ist firstname und lastname gleich location_city: drizzleBusinessListing.city,
return { firstname: parts[0], lastname: parts[0] }; location_state: drizzleBusinessListing.state,
} else { location_latitude: drizzleBusinessListing.latitude,
// Ansonsten ist der letzte Teil der lastname, der Rest der firstname location_longitude: drizzleBusinessListing.longitude,
const lastname = parts.pop()!; ...drizzleBusinessListing,
const firstname = parts.join(' '); };
return { firstname, lastname }; delete o.city;
} delete o.state;
delete o.latitude;
delete o.longitude;
return unflattenObject(o);
}
export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
return flattenObject(commercialPropertyListing);
}
export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): CommercialPropertyListing {
const o = {
location_city: drizzleCommercialPropertyListing.city,
location_state: drizzleCommercialPropertyListing.state,
location_latitude: drizzleCommercialPropertyListing.latitude,
location_longitude: drizzleCommercialPropertyListing.longitude,
...drizzleCommercialPropertyListing,
};
delete o.city;
delete o.state;
delete o.latitude;
delete o.longitude;
return unflattenObject(o);
}
export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser {
return flattenObject(user);
}
export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): User {
const o = {
companyLocation_city: drizzleUser.city,
companyLocation_state: drizzleUser.state,
companyLocation_latitude: drizzleUser.latitude,
companyLocation_longitude: drizzleUser.longitude,
...drizzleUser,
};
delete o.city;
delete o.state;
delete o.latitude;
delete o.longitude;
return unflattenObject(o);
}
function flattenObject(obj: any, res: any = {}): any {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
if (value instanceof Date) {
res[key] = value;
} else {
flattenObject(value, res);
}
} else {
res[key] = value;
}
}
}
return res;
}
function unflattenObject(obj: any, separator: string = '_'): any {
const result: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const keys = key.split(separator);
keys.reduce((acc, curr, idx) => {
if (idx === keys.length - 1) {
acc[curr] = obj[key];
} else {
if (!acc[curr]) {
acc[curr] = {};
}
}
return acc[curr];
}, result);
}
}
return result;
} }

View File

@ -1,16 +0,0 @@
import { Request } from 'express';
export interface RealIpInfo {
ip: string | undefined;
countryCode?: string;
}
export function getRealIpInfo(req: Request): RealIpInfo {
const ip =
(req.headers['cf-connecting-ip'] as string) ||
(req.headers['x-real-ip'] as string) ||
(typeof req.headers['x-forwarded-for'] === 'string' ? req.headers['x-forwarded-for'].split(',')[0] : req.connection.remoteAddress);
const countryCode = req.headers['cf-ipcountry'] as string;
return { ip, countryCode };
}

View File

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

View File

@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2021",
"module": "NodeNext", "module": "ESNext",
"moduleResolution": "NodeNext", "moduleResolution": "Node",
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
@ -18,6 +18,6 @@
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": false, "noFallthroughCasesInSwitch": false,
"esModuleInterop": true "esModuleInterop":true
} }
} }

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Under Construction</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Welcome to bizmatch.net!</h1>
<p>We're currently under construction to bring you a new and improved experience. Our website is diligently being developed to ensure that we meet your needs with the highest quality of service.</p>
<p>Please check back soon for updates. In the meantime, feel free to <a href="mailto:info@bizmatch.net">contact us</a> for any inquiries or further information.</p>
<p>Thank you for your patience and support!</p>
<p>The bizmatch.net Team</p>
</div>
</body>
</html>

52
bizmatch-static/style.css Normal file
View File

@ -0,0 +1,52 @@
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #e6f7ff; /* Hintergrundfarbe leicht blau */
color: #05386b; /* Dunkelblau für Text */
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background-image: url(./index-bg.webp);
background-size: cover;
background-position: center;
height: 100vh;
}
.container {
max-width: 600px;
padding: 40px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 10px;
border-left: 5px solid #379683; /* Grüne Akzentlinie links */
}
h1 {
color: #379683; /* Grünton für Überschriften */
}
p {
line-height: 1.6;
margin: 20px 0;
}
a {
color: #5cdb95; /* Helles Grün für Links */
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
}

View File

@ -27,17 +27,12 @@
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
{
"glob": "**/*",
"input": "public"
},
"src/favicon.ico", "src/favicon.ico",
"src/assets" "src/assets"
], ],
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
"node_modules/quill/dist/quill.snow.css", "node_modules/quill/dist/quill.snow.css"
"node_modules/leaflet/dist/leaflet.css"
] ]
}, },
"configurations": { "configurations": {
@ -46,7 +41,7 @@
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kb", "maximumWarning": "500kb",
"maximumError": "2mb" "maximumError": "1mb"
}, },
{ {
"type": "anyComponentStyle", "type": "anyComponentStyle",
@ -71,17 +66,6 @@
"optimization": false, "optimization": false,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true "sourceMap": true
},
"prod": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"extractLicenses": false,
"sourceMap": true
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"
@ -97,9 +81,7 @@
} }
}, },
"defaultConfiguration": "development", "defaultConfiguration": "development",
"options": { "options": {"proxyConfig": "proxy.conf.json"}
"proxyConfig": "proxy.conf.json"
}
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
@ -120,12 +102,7 @@
"src/assets", "src/assets",
"cropped-Favicon-32x32.png", "cropped-Favicon-32x32.png",
"cropped-Favicon-180x180.png", "cropped-Favicon-180x180.png",
"cropped-Favicon-191x192.png", "cropped-Favicon-191x192.png"
{
"glob": "**/*",
"input": "./node_modules/leaflet/dist/images",
"output": "assets/"
}
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"

View File

@ -7,7 +7,6 @@
"prebuild": "node version.js", "prebuild": "node version.js",
"build": "node version.js && ng build", "build": "node version.js && ng build",
"build.dev": "node version.js && ng build --configuration dev --output-hashing=all", "build.dev": "node version.js && ng build --configuration dev --output-hashing=all",
"build.prod": "node version.js && ng build --configuration prod --output-hashing=all",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs" "serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs"
@ -19,41 +18,34 @@
"@angular/common": "^18.1.3", "@angular/common": "^18.1.3",
"@angular/compiler": "^18.1.3", "@angular/compiler": "^18.1.3",
"@angular/core": "^18.1.3", "@angular/core": "^18.1.3",
"@angular/fire": "^18.0.1",
"@angular/forms": "^18.1.3", "@angular/forms": "^18.1.3",
"@angular/platform-browser": "^18.1.3", "@angular/platform-browser": "^18.1.3",
"@angular/platform-browser-dynamic": "^18.1.3", "@angular/platform-browser-dynamic": "^18.1.3",
"@angular/platform-server": "^18.1.3", "@angular/platform-server": "^18.1.3",
"@angular/router": "^18.1.3", "@angular/router": "^18.1.3",
"@bluehalo/ngx-leaflet": "^18.0.2",
"@fortawesome/angular-fontawesome": "^0.15.0", "@fortawesome/angular-fontawesome": "^0.15.0",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.5.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@ng-select/ng-select": "^13.4.1", "@ng-select/ng-select": "^13.4.1",
"@ngneat/until-destroy": "^10.0.0", "@ngneat/until-destroy": "^10.0.0",
"@stripe/stripe-js": "^4.3.0",
"@types/cropperjs": "^1.3.0", "@types/cropperjs": "^1.3.0",
"@types/leaflet": "^1.9.12",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"browser-bunyan": "^1.8.0", "browser-bunyan": "^1.8.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"express": "^4.18.2", "express": "^4.18.2",
"flowbite": "^2.4.1", "flowbite": "^2.4.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"leaflet": "^1.9.4", "keycloak-angular": "^16.0.1",
"keycloak-js": "^25.0.1",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"ng-gallery": "^11.0.0",
"ngx-currency": "^18.0.0", "ngx-currency": "^18.0.0",
"ngx-image-cropper": "^8.0.0", "ngx-image-cropper": "^8.0.0",
"ngx-mask": "^18.0.0", "ngx-mask": "^18.0.0",
"ngx-quill": "^26.0.5", "ngx-quill": "^26.0.5",
"ngx-sharebuttons": "^15.0.3",
"ngx-stripe": "^18.1.0",
"on-change": "^5.0.1", "on-change": "^5.0.1",
"posthog-js": "^1.259.0",
"quill": "2.0.2",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tslib": "^2.6.3", "tslib": "^2.6.3",
"urlcat": "^3.1.0", "urlcat": "^3.1.0",

View File

@ -1,28 +1,10 @@
{ {
"/bizmatch": { "/api": {
"target": "http://localhost:3001", "target": "http://localhost:3000",
"secure": false, "secure": false
"changeOrigin": true,
"logLevel": "debug"
}, },
"/pictures": { "/pictures": {
"target": "http://localhost:8080", "target": "http://localhost:8080",
"secure": false "secure": false
},
"/ipify": {
"target": "https://api.ipify.org",
"secure": true,
"changeOrigin": true,
"pathRewrite": {
"^/ipify": ""
}
},
"/ipinfo": {
"target": "https://ipinfo.io",
"secure": true,
"changeOrigin": true,
"pathRewrite": {
"^/ipinfo": ""
}
} }
} }

View File

@ -1,18 +1,10 @@
<!-- <div class="container"> --> <!-- <div class="container"> -->
<div class="wrapper" [ngClass]="{ 'print:bg-white': actualRoute !== 'home' }"> <div [ngClass]="{ 'bg-slate-100': actualRoute !== 'home' }">
@if (actualRoute !=='home' && actualRoute !=='login' && actualRoute!=='emailVerification' && actualRoute!=='email-authorized'){ @if (actualRoute !=='home' && actualRoute !=='pricing'){
<header></header> <header></header>
} }
<main class="flex-1 flex">
@if (isFilterRoute()) {
<div class="hidden md:block w-1/4 bg-white shadow-lg p-6 overflow-y-auto">
<app-search-modal [isModal]="false"></app-search-modal>
</div>
}
<div [ngClass]="{ 'w-full': !isFilterRoute(), 'md:w-3/4': isFilterRoute() }">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div>
</main>
<app-footer></app-footer> <app-footer></app-footer>
</div> </div>
@ -38,9 +30,26 @@
</div> </div>
</div> </div>
} }
<!-- <div *ngIf="loadingService.isLoading$ | async" class="spinner-overlay">
<div class="spinner-container">
<ng-container *ngIf="loadingService.loadingText$ | async as loadingText">
<div *ngIf="loadingText" class="spinner-text">{{ loadingText }}</div>
</ng-container>
<div role="status">
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div> -->
<app-message-container></app-message-container> <app-message-container></app-message-container>
<app-search-modal></app-search-modal> <app-search-modal></app-search-modal>
<app-search-modal-commercial></app-search-modal-commercial>
<app-confirmation></app-confirmation> <app-confirmation></app-confirmation>
<app-email></app-email>

View File

@ -1,3 +1,25 @@
// .progress-spinner {
// position: fixed;
// z-index: 999;
// top: 0;
// left: 0;
// bottom: 0;
// right: 0;
// display: flex;
// flex-direction: column;
// align-items: center;
// }
// .progress-spinner:before {
// content: '';
// display: block;
// position: fixed;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// background-color: rgba(0, 0, 0, 0.3);
// }
.spinner-text { .spinner-text {
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */ margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */ font-size: 20px; /* Schriftgröße nach Bedarf anpassen */

View File

@ -1,26 +1,23 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core'; import { Component, HostListener } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import build from '../build'; import build from '../build';
import { ConfirmationComponent } from './components/confirmation/confirmation.component'; import { ConfirmationComponent } from './components/confirmation/confirmation.component';
import { ConfirmationService } from './components/confirmation/confirmation.service'; import { ConfirmationService } from './components/confirmation/confirmation.service';
import { EMailComponent } from './components/email/email.component';
import { FooterComponent } from './components/footer/footer.component'; import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component'; import { MessageContainerComponent } from './components/message/message-container.component';
import { SearchModalCommercialComponent } from './components/search-modal/search-modal-commercial.component';
import { SearchModalComponent } from './components/search-modal/search-modal.component'; import { SearchModalComponent } from './components/search-modal/search-modal.component';
import { AuditService } from './services/audit.service';
import { GeoService } from './services/geo.service';
import { LoadingService } from './services/loading.service'; import { LoadingService } from './services/loading.service';
import { UserService } from './services/user.service'; import { UserService } from './services/user.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, SearchModalCommercialComponent, ConfirmationComponent, EMailComponent], imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
providers: [], providers: [],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
@ -34,10 +31,9 @@ export class AppComponent {
public loadingService: LoadingService, public loadingService: LoadingService,
private router: Router, private router: Router,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private keycloakService: KeycloakService,
private userService: UserService, private userService: UserService,
private confirmationService: ConfirmationService, private confirmationService: ConfirmationService,
private auditService: AuditService,
private geoService: GeoService,
) { ) {
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => { this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root; let currentRoute = this.activatedRoute.root;
@ -51,15 +47,17 @@ export class AppComponent {
ngOnInit() {} ngOnInit() {}
@HostListener('window:keydown', ['$event']) @HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) { handleKeyboardEvent(event: KeyboardEvent) {
// this.router.events.subscribe(event => {
// if (event instanceof NavigationEnd) {
// initFlowbite();
// }
// });
if (event.shiftKey && event.ctrlKey && event.key === 'V') { if (event.shiftKey && event.ctrlKey && event.key === 'V') {
this.showVersionDialog(); this.showVersionDialog();
} }
} }
showVersionDialog() { showVersionDialog() {
this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' }); this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
} }
isFilterRoute(): boolean {
const filterRoutes = ['/businessListings', '/commercialPropertyListings', '/brokerListings'];
return filterRoutes.includes(this.actualRoute);
}
} }

View File

@ -1,22 +1,15 @@
import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core'; import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router'; import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth';
import { provideAnimations } from '@angular/platform-browser/animations'; import { provideAnimations } from '@angular/platform-browser/animations';
import { GALLERY_CONFIG, GalleryConfig } from 'ng-gallery'; import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
import { provideQuillConfig } from 'ngx-quill'; import { provideQuillConfig } from 'ngx-quill';
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
import { shareIcons } from 'ngx-sharebuttons/icons';
import { provideNgxStripe } from 'ngx-stripe';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { customKeycloakAdapter } from '../keycloak';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { AuthInterceptor } from './interceptors/auth.interceptor';
import { LoadingInterceptor } from './interceptors/loading.interceptor'; import { LoadingInterceptor } from './interceptors/loading.interceptor';
import { TimeoutInterceptor } from './interceptors/timeout.interceptor'; import { KeycloakInitializerService } from './services/keycloak-initializer.service';
import { GlobalErrorHandler } from './services/globalErrorHandler';
import { POSTHOG_INIT_PROVIDER } from './services/posthog.factory';
import { SelectOptionsService } from './services/select-options.service'; import { SelectOptionsService } from './services/select-options.service';
import { createLogger } from './utils/utils'; import { createLogger } from './utils/utils';
// provideClientHydration() // provideClientHydration()
@ -24,6 +17,16 @@ const logger = createLogger('ApplicationConfig');
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
{ provide: KeycloakService },
{
provide: APP_INITIALIZER,
// useFactory: initializeKeycloak,
//useFactory: initializeKeycloak,
useFactory: initializeKeycloak3,
multi: true,
//deps: [KeycloakService],
deps: [KeycloakInitializerService],
},
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initServices, useFactory: initServices,
@ -37,29 +40,9 @@ export const appConfig: ApplicationConfig = {
}, },
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: TimeoutInterceptor, useClass: KeycloakBearerInterceptor,
multi: true, multi: true,
}, },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{
provide: 'TIMEOUT_DURATION',
useValue: 5000, // Standard-Timeout von 5 Sekunden
},
{
provide: GALLERY_CONFIG,
useValue: {
autoHeight: true,
imageSize: 'cover',
} as GalleryConfig,
},
{ provide: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler
provideShareButtonsOptions(
shareIcons(),
withConfig({
debug: true,
sharerMethod: SharerMethods.Anchor,
}),
),
provideRouter( provideRouter(
routes, routes,
withEnabledBlockingInitialNavigation(), withEnabledBlockingInitialNavigation(),
@ -68,9 +51,7 @@ export const appConfig: ApplicationConfig = {
anchorScrolling: 'enabled', anchorScrolling: 'enabled',
}), }),
), ),
...(environment.production ? [POSTHOG_INIT_PROVIDER] : []),
provideAnimations(), provideAnimations(),
provideNgxStripe('pk_test_IlpbVQhxAXZypLgnCHOCqlj8'),
provideQuillConfig({ provideQuillConfig({
modules: { modules: {
syntax: true, syntax: true,
@ -83,9 +64,6 @@ export const appConfig: ApplicationConfig = {
], ],
}, },
}), }),
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
provideAuth(() => getAuth()),
// provideFirestore(() => getFirestore()),
], ],
}; };
function initServices(selectOptions: SelectOptionsService) { function initServices(selectOptions: SelectOptionsService) {
@ -93,3 +71,47 @@ function initServices(selectOptions: SelectOptionsService) {
await selectOptions.init(); await selectOptions.init();
}; };
} }
export function initializeKeycloak3(keycloak: KeycloakInitializerService) {
return () => keycloak.initialize();
}
export function initializeKeycloak2(keycloak: KeycloakService): () => Promise<void> {
return async () => {
const { url, realm, clientId } = environment.keycloak;
const adapter = customKeycloakAdapter(() => keycloak.getKeycloakInstance(), {});
if (window.location.search.length > 0) {
sessionStorage.setItem('SEARCH', window.location.search);
}
const { host, hostname, href, origin, pathname, port, protocol, search } = window.location;
await keycloak.init({
config: { url, realm, clientId },
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.hostname === 'localhost' ? `${window.location.origin}/assets/silent-check-sso.html` : `${window.location.origin}/dealerweb/assets/silent-check-sso.html`,
adapter,
redirectUri: `${origin}${pathname}`,
},
});
};
}
function initializeKeycloak(keycloak: KeycloakService) {
return async () => {
logger.info(`###>calling keycloakService init ...`);
const authenticated = await keycloak.init({
config: {
url: environment.keycloak.url,
realm: environment.keycloak.realm,
clientId: environment.keycloak.clientId,
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
},
bearerExcludedUrls: ['/assets'],
shouldUpdateToken(request) {
return !request.headers.get('token-update') === false;
},
});
logger.info(`+++>${authenticated}`);
};
}

View File

@ -2,12 +2,8 @@ import { Routes } from '@angular/router';
import { LogoutComponent } from './components/logout/logout.component'; import { LogoutComponent } from './components/logout/logout.component';
import { NotFoundComponent } from './components/not-found/not-found.component'; import { NotFoundComponent } from './components/not-found/not-found.component';
import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component';
import { EmailVerificationComponent } from './components/email-verification/email-verification.component';
import { LoginRegisterComponent } from './components/login-register/login-register.component';
import { AuthGuard } from './guards/auth.guard'; import { AuthGuard } from './guards/auth.guard';
import { ListingCategoryGuard } from './guards/listing-category.guard'; import { ListingCategoryGuard } from './guards/listing-category.guard';
import { UserListComponent } from './pages/admin/user-list/user-list.component';
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component'; import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component'; import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
import { DetailsUserComponent } from './pages/details/details-user/details-user.component'; import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
@ -22,7 +18,6 @@ import { EditCommercialPropertyListingComponent } from './pages/subscription/edi
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component'; import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component'; import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component'; import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
import { SuccessComponent } from './pages/success/success.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -59,22 +54,6 @@ export const routes: Routes = [
canActivate: [ListingCategoryGuard], canActivate: [ListingCategoryGuard],
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
}, },
// {
// path: 'login/:page',
// component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
// },
{
path: 'login/:page',
component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
},
{
path: 'login',
component: LoginRegisterComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
},
{
path: 'notfound',
component: NotFoundComponent,
},
// ######### // #########
// User Details // User Details
{ {
@ -134,7 +113,7 @@ export const routes: Routes = [
{ {
path: 'emailUs', path: 'emailUs',
component: EmailUsComponent, component: EmailUsComponent,
// canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },
// ######### // #########
// Logout // Logout
@ -149,33 +128,5 @@ export const routes: Routes = [
path: 'pricing', path: 'pricing',
component: PricingComponent, component: PricingComponent,
}, },
{
path: 'emailVerification',
component: EmailVerificationComponent,
},
{
path: 'email-authorized',
component: EmailAuthorizedComponent,
},
{
path: 'pricingOverview',
component: PricingComponent,
data: {
pricingOverview: true,
},
},
{
path: 'pricing/:id',
component: PricingComponent,
},
{
path: 'success',
component: SuccessComponent,
},
{
path: 'admin/users',
component: UserListComponent,
canActivate: [AuthGuard],
},
{ path: '**', redirectTo: 'home' }, { path: '**', redirectTo: 'home' },
]; ];

View File

@ -19,7 +19,6 @@ export abstract class BaseInputComponent implements ControlValueAccessor {
@Input() label: string = ''; @Input() label: string = '';
// @Input() id: string = ''; // @Input() id: string = '';
@Input() name: string = ''; @Input() name: string = '';
isTooltipVisible = false;
constructor(protected validationMessagesService: ValidationMessagesService) {} constructor(protected validationMessagesService: ValidationMessagesService) {}
ngOnInit() { ngOnInit() {
this.subscription = this.validationMessagesService.messages$.subscribe(() => { this.subscription = this.validationMessagesService.messages$.subscribe(() => {
@ -52,9 +51,4 @@ export abstract class BaseInputComponent implements ControlValueAccessor {
this.validationMessage = this.validationMessagesService.getMessage(this.name); this.validationMessage = this.validationMessagesService.getMessage(this.name);
} }
setDisabledState?(isDisabled: boolean): void {} setDisabledState?(isDisabled: boolean): void {}
toggleTooltip(event: Event) {
event.preventDefault();
event.stopPropagation();
this.isTooltipVisible = !this.isTooltipVisible;
}
} }

View File

@ -25,8 +25,8 @@ import { ConfirmationService } from './confirmation.service';
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg> </svg>
@let confirmation = (confirmationService.confirmation$ | async); @let confirmation = (confirmationService.confirmation$ | async);
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation?.message }}</h3> <h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation.message }}</h3>
@if(confirmation?.buttons==='both'){ @if(confirmation.buttons==='both'){
<button <button
(click)="confirmationService.accept()" (click)="confirmationService.accept()"
type="button" type="button"

View File

@ -1,29 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { FormsModule } from '@angular/forms';
interface KeyValue {
name: string;
value: string;
}
@Component({
selector: 'app-customer-sub-type',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<ng-container [ngSwitch]="customerSubType">
<span *ngSwitchCase="'broker'" class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-blue-400 border border-blue-400">Broker</span>
<span *ngSwitchCase="'cpa'" class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-400 border border-gray-500">CPA</span>
<span *ngSwitchCase="'attorney'" class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">Attorney</span>
<span *ngSwitchCase="'titleCompany'" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Title Company</span>
<span *ngSwitchCase="'surveyor'" class="bg-yellow-100 text-yellow-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-yellow-300 border border-yellow-300">Surveyor</span>
<span *ngSwitchCase="'appraiser'" class="bg-pink-100 text-pink-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-pink-400 border border-pink-400">Appraiser</span>
<span *ngSwitchDefault class="text-gray-500">Unknown</span>
</ng-container>
`,
styles: [],
})
export class CustomerSubTypeComponent {
@Input() customerSubType: 'broker' | 'cpa' | 'attorney' | 'surveyor' | 'appraiser' | 'titleCompany';
}

View File

@ -1,8 +1,20 @@
<div #_container class="container"> <div #_container class="container">
<!-- <div
*ngFor="let item of items"
cdkDrag
(cdkDragEnded)="dragEnded($event)"
(cdkDragStarted)="dragStarted()"
(cdkDragMoved)="dragMoved($event)"
class="item"
[class.animation]="isAnimationActive"
[class.large]="item === 3"
>
Drag Item {{ item }}
</div> -->
<div *ngFor="let item of items" cdkDrag (cdkDragEnded)="dragEnded($event)" (cdkDragStarted)="dragStarted()" (cdkDragMoved)="dragMoved($event)" [class.animation]="isAnimationActive" class="grid-item item"> <div *ngFor="let item of items" cdkDrag (cdkDragEnded)="dragEnded($event)" (cdkDragStarted)="dragStarted()" (cdkDragMoved)="dragMoved($event)" [class.animation]="isAnimationActive" class="grid-item item">
<div class="image-box hover:cursor-pointer"> <div class="image-box hover:cursor-pointer">
<img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg drop-shadow-custom-bg" /> <img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg shadow-md" />
<div class="absolute top-2 right-2 bg-white rounded-full p-1 drop-shadow-custom-bg" (click)="imageToDelete.emit(item)"> <div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md" (click)="imageToDelete.emit(item)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>

View File

@ -1,35 +0,0 @@
<div class="container mx-auto py-8 px-4 max-w-md">
<div class="bg-white p-6 rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg text-center">
<!-- Loading state -->
<ng-container *ngIf="verificationStatus === 'pending'">
<div class="flex justify-center mb-4">
<div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<p class="text-gray-700">Verifying your email address...</p>
</ng-container>
<!-- Success state -->
<ng-container *ngIf="verificationStatus === 'success'">
<div class="flex justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-2xl font-bold text-green-600 mb-5">Your email has been verified</h2>
<p class="text-gray-700 mb-4">You will be redirected to your account page in 5 seconds</p>
<a routerLink="/account" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Go to Account Page Now </a>
</ng-container>
<!-- Error state -->
<ng-container *ngIf="verificationStatus === 'error'">
<div class="flex justify-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 class="text-2xl font-bold text-red-600 mb-3">Verification Failed</h2>
<p class="text-gray-700 mb-4">{{ errorMessage }}</p>
<a routerLink="/login" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Return to Login </a>
</ng-container>
</div>
</div>

View File

@ -1,68 +0,0 @@
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { environment } from '../../../environments/environment';
import { AuthService } from '../../services/auth.service';
import { UserService } from '../../services/user.service';
@Component({
selector: 'app-email-authorized',
standalone: true,
imports: [CommonModule, RouterModule],
templateUrl: './email-authorized.component.html',
})
export class EmailAuthorizedComponent implements OnInit {
verificationStatus: 'pending' | 'success' | 'error' = 'pending';
errorMessage: string | null = null;
constructor(private route: ActivatedRoute, private router: Router, private http: HttpClient, private authService: AuthService, private userService: UserService) {}
ngOnInit(): void {
const oobCode = this.route.snapshot.queryParamMap.get('oobCode');
const email = this.route.snapshot.queryParamMap.get('email');
const mode = this.route.snapshot.queryParamMap.get('mode');
if (mode === 'verifyEmail' && oobCode && email) {
this.verifyEmail(oobCode, email);
} else {
this.verificationStatus = 'error';
this.errorMessage = 'Invalid verification link';
}
}
private verifyEmail(oobCode: string, email: string): void {
this.http.post<{ message: string; token: string }>(`${environment.apiBaseUrl}/bizmatch/auth/verify-email`, { oobCode, email }).subscribe({
next: async response => {
this.verificationStatus = 'success';
try {
// Use the custom token from the server to sign in with Firebase
await this.authService.signInWithCustomToken(response.token);
// Try to get user info
try {
const user = await this.userService.getByMail(email);
console.log('User retrieved:', user);
} catch (userError) {
console.error('Error getting user:', userError);
// Don't change verification status - it's still a success
}
// Redirect to dashboard after a short delay
setTimeout(() => {
this.router.navigate(['/account']);
}, 5000);
} catch (authError) {
console.error('Error signing in with custom token:', authError);
// Keep success status for verification, but add warning about login
this.errorMessage = 'Email verified, but there was an issue signing you in. Please try logging in manually.';
}
},
error: err => {
this.verificationStatus = 'error';
this.errorMessage = err.error?.message || 'Verification failed';
},
});
}
}

View File

@ -1,7 +0,0 @@
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<div class="bg-white p-8 rounded drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg w-full max-w-md text-center">
<h2 class="text-2xl font-bold mb-4">Email Verification</h2>
<p class="mb-4">A verification email has been sent to your email address. Please check your inbox and click the link to verify your account.</p>
<p>Once verified, please return to the application.</p>
</div>
</div>

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