Compare commits
120 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
c2d7a53039 | |
|
|
4fa24c8f3d | |
|
|
351b560bcc | |
|
|
f973b87a2d | |
|
|
995468fa30 | |
|
|
6fa3bea614 | |
|
|
6b12e0cbac | |
|
|
39b579ea4e | |
|
|
8113206e90 | |
|
|
3b51a98dec | |
|
|
fbca2ddab5 | |
|
|
03d075b7d9 | |
|
|
f9d4506bde | |
|
|
571cfb0e61 | |
|
|
d48cd7aa1d | |
|
|
bab898adf4 | |
|
|
8dff7eca6a | |
|
|
418cc3a043 | |
|
|
7b94785a30 | |
|
|
c5c210b616 | |
|
|
4dcff1d883 | |
|
|
4efa6c9d77 | |
|
|
4d74c20c87 | |
|
|
8624c1b8da | |
|
|
93ff8c3378 | |
|
|
738f1d929b | |
|
|
9c88143c04 | |
|
|
569e086bb4 | |
|
|
dda1b2f54d | |
|
|
d14f333991 | |
|
|
388aac5a76 | |
|
|
2ebe6454ec | |
|
|
903ca7dc56 | |
|
|
5619007b0f | |
|
|
f3bf6ff9af | |
|
|
c62af8746f | |
|
|
7d336f975d | |
|
|
e913026f53 | |
|
|
a6f1571b8b | |
|
|
01b5679e54 | |
|
|
24db8927e8 | |
|
|
466e1dcdce | |
|
|
7d64ee11bf | |
|
|
83808263af | |
|
|
b39370a6b5 | |
|
|
6b97008643 | |
|
|
715fbdf2f5 | |
|
|
923040f487 | |
|
|
cfddabbfe0 | |
|
|
097a6cb360 | |
|
|
162c5b042f | |
|
|
9e8f67d647 | |
|
|
5a56b3554d | |
|
|
dded8b8ca9 | |
|
|
b55447cd3f | |
|
|
e37613ffa0 | |
|
|
d8c48bf58a | |
|
|
4c19356188 | |
|
|
27242819e2 | |
|
|
521e799bff | |
|
|
f6d1b8623c | |
|
|
a2e6243e93 | |
|
|
b9a9b983e9 | |
|
|
1282d30b49 | |
|
|
974a6503ef | |
|
|
860d30b16f | |
|
|
178f2b4810 | |
|
|
bb26972377 | |
|
|
16b880384b | |
|
|
3e84b82c92 | |
|
|
205793faab | |
|
|
eaa8a5064f | |
|
|
c00c2caccc | |
|
|
8595e70ceb | |
|
|
8dd13d5472 | |
|
|
f36d9fb4d7 | |
|
|
eb5a334868 | |
|
|
446d568378 | |
|
|
d4ec9d067f | |
|
|
7c9a47cf4e | |
|
|
40ba402c70 | |
|
|
d2f6b3ec3f | |
|
|
77c9973256 | |
|
|
68d2615f0f | |
|
|
60866473f7 | |
|
|
8a7e26d2b6 | |
|
|
fe759f953f | |
|
|
83307684ee | |
|
|
17213ba4b0 | |
|
|
24ed50a48f | |
|
|
06d83a478d | |
|
|
9ecc0c2429 | |
|
|
7807afbad3 | |
|
|
624fa74eb6 | |
|
|
3b012a8113 | |
|
|
d8429f9b4a | |
|
|
c5577969c8 | |
|
|
f4f576d4a9 | |
|
|
630c31cfc9 | |
|
|
ede8b66d83 | |
|
|
8721be4a90 | |
|
|
c1b72bbc12 | |
|
|
0f301fb534 | |
|
|
8157dcc376 | |
|
|
f66badbfb1 | |
|
|
74d5f92aba | |
|
|
7a286e3519 | |
|
|
b4609d07ba | |
|
|
48bff89526 | |
|
|
056db7b199 | |
|
|
8c6c6e3dbd | |
|
|
7f756a71e8 | |
|
|
a8bb163acf | |
|
|
1f8febc479 | |
|
|
ec0576e7b8 | |
|
|
3a6a64cce9 | |
|
|
245e76f697 | |
|
|
d71a5c25c3 | |
|
|
1e1d5cea57 | |
|
|
6d1c50d5df |
|
|
@ -0,0 +1,195 @@
|
|||
# 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.
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
/data
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
@ -57,6 +58,12 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|||
|
||||
pictures
|
||||
pictures_base
|
||||
pictures_
|
||||
|
||||
src/*.js
|
||||
bun.lockb
|
||||
|
||||
#drizzle migrations
|
||||
src/drizzle/migrations
|
||||
|
||||
importlog.txt
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Nest Framework",
|
||||
"runtimeExecutable": "npm",
|
||||
//"runtimeExecutable": "npm",
|
||||
"runtimeExecutable": "/home/aknuth/.nvm/versions/node/v22.14.0/bin/npm",
|
||||
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"restart": true,
|
||||
|
|
@ -13,16 +14,20 @@
|
|||
"stopOnEntry": false,
|
||||
"console": "integratedTerminal",
|
||||
"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",
|
||||
"request": "launch",
|
||||
"name": "Launch TypeScript file with tsx",
|
||||
"name": "Launch import from exported with tsx",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["tsx", "--inspect"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/importFromExported.ts"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"],
|
||||
"sourceMaps": true,
|
||||
|
|
@ -60,5 +65,30 @@
|
|||
"sourceMaps": 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": "."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# 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"]
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
/* 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,
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# ~/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
|
||||
|
|
@ -3,7 +3,6 @@ export default defineConfig({
|
|||
schema: './src/drizzle/schema.ts',
|
||||
out: './src/drizzle/migrations',
|
||||
dialect: 'postgresql',
|
||||
// driver: 'pg',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,14 +5,14 @@
|
|||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "HOST_NAME=localhost nest start",
|
||||
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
|
||||
"start": "nest start",
|
||||
"start:local": "HOST_NAME=localhost node dist/src/main",
|
||||
"start:dev": "NODE_ENV=development node dist/src/main",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
|
||||
"start:prod": "NODE_ENV=production node dist/src/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
|
|
@ -26,35 +26,33 @@
|
|||
"generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.10.3",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs-modules/mailer": "^2.0.2",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/config": "^4.0.0",
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/cli": "^11.0.11",
|
||||
"@nestjs/platform-express": "^11.0.11",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"body-parser": "^1.20.2",
|
||||
"cls-hooked": "^4.2.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.32.0",
|
||||
"firebase": "^11.3.1",
|
||||
"firebase-admin": "^13.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"ky": "^1.4.0",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nestjs-cls": "^5.4.0",
|
||||
"nodemailer": "^6.9.10",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"openai": "^4.52.6",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^8.11.5",
|
||||
"pgvector": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.2",
|
||||
"stripe": "^16.8.0",
|
||||
"tsx": "^4.16.2",
|
||||
"urlcat": "^3.1.0",
|
||||
"winston": "^3.11.0",
|
||||
|
|
@ -63,29 +61,22 @@
|
|||
"devDependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/traverse": "^7.24.1",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@nestjs/cli": "^11.0.5",
|
||||
"@nestjs/schematics": "^11.0.1",
|
||||
"@nestjs/testing": "^11.0.11",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"drizzle-kit": "^0.23.0",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"kysely-codegen": "^0.15.0",
|
||||
"nest-commander": "^3.16.1",
|
||||
"pg-to-ts": "^4.1.1",
|
||||
"prettier": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AiService } from './ai.service.js';
|
||||
import { AiService } from './ai.service';
|
||||
|
||||
@Controller('ai')
|
||||
export class AiController {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AiController } from './ai.controller.js';
|
||||
import { AiService } from './ai.service.js';
|
||||
import { AiController } from './ai.controller';
|
||||
import { AiService } from './ai.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AiController],
|
||||
|
|
|
|||
|
|
@ -3,30 +3,85 @@ import Groq from 'groq-sdk';
|
|||
import OpenAI from 'openai';
|
||||
import { BusinessListingCriteria } from '../models/main.model';
|
||||
|
||||
const businessListingCriteriaStructure = {
|
||||
criteriaType: 'business | commercialProperty | broker',
|
||||
types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
|
||||
city: 'string',
|
||||
state: 'string',
|
||||
county: 'string',
|
||||
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',
|
||||
searchType: "'exact' | 'radius'",
|
||||
radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'",
|
||||
};
|
||||
// const businessListingCriteriaStructure = {
|
||||
// criteriaType: 'business | commercialProperty | broker',
|
||||
// types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
|
||||
// city: 'string',
|
||||
// state: 'string',
|
||||
// county: 'string',
|
||||
// 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',
|
||||
// searchType: "'exact' | 'radius'",
|
||||
// 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()
|
||||
export class AiService {
|
||||
private readonly openai: OpenAI;
|
||||
|
|
@ -46,42 +101,49 @@ export class AiService {
|
|||
const prompt = `The Search Query of the User is: "${query}"`;
|
||||
let response = null;
|
||||
try {
|
||||
// response = await this.openai.chat.completions.create({
|
||||
// model: 'gpt-4o-mini',
|
||||
// //model: 'gpt-3.5-turbo',
|
||||
// max_tokens: 300,
|
||||
// messages: [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: `Please create unformatted JSON Object from a user input.
|
||||
// The type is: ${JSON.stringify(businessListingCriteriaStructure)}.,
|
||||
// If location details available please fill city, county and state as State Code`,
|
||||
// },
|
||||
// ],
|
||||
// temperature: 0.5,
|
||||
// response_format: { type: 'json_object' },
|
||||
// });
|
||||
|
||||
response = await this.groq.chat.completions.create({
|
||||
response = await this.openai.chat.completions.create({
|
||||
model: 'gpt-4o-mini',
|
||||
//model: 'gpt-3.5-turbo',
|
||||
max_tokens: 300,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `Please create unformatted JSON Object from a user input.
|
||||
The type must be: ${JSON.stringify(businessListingCriteriaStructure)}.
|
||||
If location details available please fill city, county and state as State Code`,
|
||||
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.1-70b-versatile',
|
||||
//model: 'llama-3.1-8b-instant',
|
||||
temperature: 0.2,
|
||||
max_tokens: 300,
|
||||
temperature: 0.5,
|
||||
response_format: { type: 'json_object' },
|
||||
});
|
||||
|
||||
// response = await this.groq.chat.completions.create({
|
||||
// messages: [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: `Please create unformatted JSON Object from a user input.
|
||||
// The criteriaType must be only either 'businessListings' or 'commercialPropertyListings' or 'brokerListings' !!!!
|
||||
// The format of the object (depending on your choice of criteriaType) must be either ${BusinessListingCriteriaStructure}, ${CommercialPropertyListingCriteriaStructure} or ${UserListingCriteriaStructure} !!!!
|
||||
// If location details available please fill city and state as State Code and only county if explicitly mentioned.
|
||||
// If you decide for searchType==='exact', please do not set the attribute radius`,
|
||||
// },
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: prompt,
|
||||
// },
|
||||
// ],
|
||||
// model: 'llama-3.3-70b-versatile',
|
||||
// temperature: 0.2,
|
||||
// max_tokens: 300,
|
||||
// response_format: { type: 'json_object' },
|
||||
// });
|
||||
|
||||
const generatedCriteria = JSON.parse(response.choices[0]?.message?.content);
|
||||
return generatedCriteria;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||
import { AppService } from './app.service.js';
|
||||
import { AuthService } from './auth/auth.service.js';
|
||||
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
import { AuthGuard } from './jwt-auth/auth.guard';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(
|
||||
private readonly appService: AppService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Get()
|
||||
getHello(@Request() req): string {
|
||||
return req.user;
|
||||
//return 'dfgdf';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,54 @@
|
|||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import * as dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import { AiModule } from './ai/ai.module.js';
|
||||
import { AppController } from './app.controller.js';
|
||||
import { AppService } from './app.service.js';
|
||||
import { AuthModule } from './auth/auth.module.js';
|
||||
import { FileService } from './file/file.service.js';
|
||||
import { GeoModule } from './geo/geo.module.js';
|
||||
import { ImageModule } from './image/image.module.js';
|
||||
import { ListingsModule } from './listings/listings.module.js';
|
||||
import { MailModule } from './mail/mail.module.js';
|
||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||
import { UserModule } from './user/user.module.js';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
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';
|
||||
|
||||
function loadEnvFiles() {
|
||||
// Load the .env file
|
||||
dotenv.config();
|
||||
console.log('Loaded .env file');
|
||||
import { EventModule } from './event/event.module';
|
||||
import { MailModule } from './mail/mail.module';
|
||||
|
||||
// Determine which additional env file to load
|
||||
let envFilePath = '';
|
||||
const host = process.env.HOST_NAME || '';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ClsMiddleware, ClsModule } from 'nestjs-cls';
|
||||
import path from 'path';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { FirebaseAdminModule } from './firebase-admin/firebase-admin.module';
|
||||
import { LoggingInterceptor } from './interceptors/logging.interceptor';
|
||||
import { UserInterceptor } from './interceptors/user.interceptor';
|
||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
|
||||
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();
|
||||
//loadEnvFiles();
|
||||
console.log('Loaded environment variables:');
|
||||
//console.log(JSON.stringify(process.env, null, 2));
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
ClsModule.forRoot({
|
||||
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,
|
||||
AuthModule,
|
||||
WinstonModule.forRoot({
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD hh:mm:ss.SSS A',
|
||||
}),
|
||||
winston.format.ms(),
|
||||
nestWinstonModuleUtilities.format.nestLike('Bizmatch', {
|
||||
colors: true,
|
||||
|
|
@ -73,14 +65,30 @@ loadEnvFiles();
|
|||
ListingsModule,
|
||||
SelectOptionsModule,
|
||||
ImageModule,
|
||||
PassportModule,
|
||||
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 {
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(ClsMiddleware).forRoutes('*');
|
||||
consumer.apply(RequestDurationMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,40 +1,139 @@
|
|||
import { Controller, Get, Param, Put } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { Body, Controller, Get, HttpException, HttpStatus, Inject, Param, Post, Query, Req, UseGuards } from '@nestjs/common';
|
||||
import * as admin from 'firebase-admin';
|
||||
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { LocalhostGuard } from 'src/jwt-auth/localhost.guard';
|
||||
import { UserRole, UsersResponse } from 'src/models/main.model';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
constructor(
|
||||
@Inject('FIREBASE_ADMIN')
|
||||
private readonly firebaseAdmin: typeof admin,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
@Post('verify-email')
|
||||
async verifyEmail(@Body('oobCode') oobCode: string, @Body('email') email: string) {
|
||||
if (!oobCode || !email) {
|
||||
throw new HttpException('oobCode and email are required', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
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()
|
||||
getAccessToken(): any {
|
||||
return this.authService.getAccessToken();
|
||||
@UseGuards(AuthGuard, AdminGuard) // Only admins can list all users
|
||||
async getAllUsers(@Query('maxResults') maxResults?: number, @Query('pageToken') pageToken?: string): Promise<UsersResponse> {
|
||||
const result = await this.authService.getAllUsers(maxResults ? parseInt(maxResults.toString(), 10) : undefined, pageToken);
|
||||
|
||||
return {
|
||||
users: result.users,
|
||||
totalCount: result.users.length,
|
||||
...(result.pageToken && { pageToken: result.pageToken }),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
getUsers(): any {
|
||||
return this.authService.getUsers();
|
||||
}
|
||||
@Get('user/:userid')
|
||||
getUser(@Param('userid') userId: string): any {
|
||||
return this.authService.getUser(userId);
|
||||
}
|
||||
@Get('groups')
|
||||
getGroups(): any {
|
||||
return this.authService.getGroups();
|
||||
}
|
||||
/**
|
||||
* Endpoint zum direkten Einstellen einer Rolle für Debug-Zwecke
|
||||
* WARNUNG: Dieser Endpoint sollte in der Produktion entfernt oder stark gesichert werden
|
||||
*/
|
||||
@Post('set-role')
|
||||
@UseGuards(AuthGuard, LocalhostGuard)
|
||||
async setUserRoleOnLocalhost(@Req() req: any, @Body('role') role: UserRole): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const uid = req.user.uid;
|
||||
|
||||
@Get('user/:userid/groups') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
getGroupsForUsers(@Param('userid') userId: string): any {
|
||||
return this.authService.getGroupsForUser(userId);
|
||||
}
|
||||
// Aktuelle Rolle protokollieren
|
||||
const currentUser = await this.authService.getUserRole(uid);
|
||||
console.log(`Changing role for user ${uid} from ${currentUser} to ${role}`);
|
||||
|
||||
@Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
getLastLogin(@Param('userid') userId: string): any {
|
||||
return this.authService.getLastLogin(userId);
|
||||
}
|
||||
// Neue Rolle setzen
|
||||
await this.authService.setUserRole(uid, role);
|
||||
|
||||
@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);
|
||||
// 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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtStrategy } from '../jwt.strategy.js';
|
||||
import { AuthController } from './auth.controller.js';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [PassportModule],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
imports: [ConfigModule.forRoot({ envFilePath: '.env' }),FirebaseAdminModule],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
providers: [AuthService],
|
||||
exports: [],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
|
|
|||
|
|
@ -1,119 +1,113 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import ky from 'ky';
|
||||
import urlcat from 'urlcat';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as admin from 'firebase-admin';
|
||||
import { FirebaseUserInfo, UserRole } from 'src/models/main.model';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
public async getAccessToken() {
|
||||
const form = new FormData();
|
||||
form.append('grant_type', 'password');
|
||||
form.append('username', process.env.user);
|
||||
form.append('password', process.env.password);
|
||||
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
|
||||
|
||||
/**
|
||||
* Set a user's role via Firebase custom claims
|
||||
*/
|
||||
async setUserRole(uid: string, role: UserRole): Promise<void> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('grant_type', 'password');
|
||||
params.append('username', process.env.user);
|
||||
params.append('password', process.env.password);
|
||||
const URL = `${process.env.host}${process.env.tokenURL}`;
|
||||
// Get the current custom claims
|
||||
const user = await this.firebaseAdmin.auth().getUser(uid);
|
||||
const currentClaims = user.customClaims || {};
|
||||
|
||||
const response = await ky
|
||||
.post(URL, {
|
||||
body: params.toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: 'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return (<any>response).access_token;
|
||||
// Set the new role
|
||||
await this.firebaseAdmin.auth().setCustomUserClaims(uid, {
|
||||
...currentClaims,
|
||||
role: role,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.name === 'HTTPError') {
|
||||
const errorJson = await error.response.json();
|
||||
console.error('Fehlerantwort vom Server:', errorJson);
|
||||
} else {
|
||||
console.error('Allgemeiner Fehler:', error);
|
||||
}
|
||||
console.error('Error setting user role:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getUsers() {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.usersURL}`;
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getUser(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.userURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getGroups() {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.groupsURL}`;
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
/**
|
||||
* Get a user's current role
|
||||
*/
|
||||
async getUserRole(uid: string): Promise<UserRole | null> {
|
||||
try {
|
||||
const user = await this.firebaseAdmin.auth().getUser(uid);
|
||||
const claims = user.customClaims || {};
|
||||
return (claims.role as UserRole) || null;
|
||||
} catch (error) {
|
||||
console.error('Error getting user role:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getGroupsForUser(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.userGroupsURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
/**
|
||||
* Get all users with a specific role
|
||||
*/
|
||||
async getUsersByRole(role: UserRole): Promise<admin.auth.UserRecord[]> {
|
||||
// Note: Firebase Admin doesn't provide a direct way to query users by custom claims
|
||||
// For a production app, you might want to store role information in Firestore as well
|
||||
// This is a simple implementation that lists all users and filters them
|
||||
try {
|
||||
const listUsersResult = await this.firebaseAdmin.auth().listUsers();
|
||||
return listUsersResult.users.filter(user => user.customClaims && user.customClaims.role === role);
|
||||
} catch (error) {
|
||||
console.error('Error getting users by role:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
/**
|
||||
* Get all Firebase users with their roles
|
||||
* @param maxResults Maximum number of users to return (optional, default 1000)
|
||||
* @param pageToken Token for pagination (optional)
|
||||
*/
|
||||
async getAllUsers(maxResults: number = 1000, pageToken?: string): Promise<{ users: FirebaseUserInfo[]; pageToken?: string }> {
|
||||
try {
|
||||
const listUsersResult = await this.firebaseAdmin.auth().listUsers(maxResults, pageToken);
|
||||
|
||||
const users = listUsersResult.users.map(user => this.mapUserRecord(user));
|
||||
|
||||
return {
|
||||
users,
|
||||
pageToken: listUsersResult.pageToken,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting all users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
public async addUser2Group(userid: string, groupid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.addUser2GroupURL, { userid, groupid });
|
||||
const response = await ky
|
||||
.put(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
// 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);
|
||||
});
|
||||
|
|
@ -1,24 +1,44 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import pkg from 'pg';
|
||||
import { Logger } from 'winston';
|
||||
import * as schema from './schema';
|
||||
import { PG_CONNECTION } from './schema';
|
||||
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({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PG_CONNECTION,
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
inject: [ConfigService, WINSTON_MODULE_PROVIDER, ClsService],
|
||||
useFactory: async (configService: ConfigService, logger: Logger, cls: ClsService) => {
|
||||
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({
|
||||
connectionString,
|
||||
// ssl: true,
|
||||
// ssl: true, // Falls benötigt
|
||||
});
|
||||
|
||||
return drizzle(pool, { schema, logger:true });
|
||||
// Definiere einen benutzerdefinierten Logger für Drizzle
|
||||
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 });
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -2,20 +2,19 @@ import 'dotenv/config';
|
|||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import OpenAI from 'openai';
|
||||
import { join } from 'path';
|
||||
import pkg from 'pg';
|
||||
import { Pool } from 'pg';
|
||||
import { rimraf } from 'rimraf';
|
||||
import sharp from 'sharp';
|
||||
import { BusinessListingService } from 'src/listings/business-listing.service.js';
|
||||
import { CommercialPropertyService } from 'src/listings/commercial-property.service.js';
|
||||
import { Geo } from 'src/models/server.model.js';
|
||||
import { BusinessListingService } from 'src/listings/business-listing.service';
|
||||
import { CommercialPropertyService } from 'src/listings/commercial-property.service';
|
||||
import { Geo } from 'src/models/server.model';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import winston from 'winston';
|
||||
import { User, UserData } from '../models/db.model.js';
|
||||
import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { convertUserToDrizzleUser } from '../utils.js';
|
||||
import * as schema from './schema.js';
|
||||
import { User, UserData } from '../models/db.model';
|
||||
import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName } from '../models/main.model';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||
import * as schema from './schema';
|
||||
interface PropertyImportListing {
|
||||
id: string;
|
||||
userId: string;
|
||||
|
|
@ -54,224 +53,226 @@ interface BusinessImportListing {
|
|||
internals: string;
|
||||
created: string;
|
||||
}
|
||||
const typesOfBusiness: Array<KeyValueStyle> = [
|
||||
{ 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: '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: '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: '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: '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: '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: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
|
||||
];
|
||||
const { Pool } = pkg;
|
||||
// const typesOfBusiness: Array<KeyValueStyle> = [
|
||||
// { 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: '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: '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: '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: '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: '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: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
|
||||
// ];
|
||||
// const { Pool } = pkg;
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen
|
||||
});
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
// const pool = new Pool({connectionString})
|
||||
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);
|
||||
//Delete Content
|
||||
await db.delete(schema.commercials);
|
||||
await db.delete(schema.businesses);
|
||||
await db.delete(schema.users);
|
||||
let filePath = `./src/assets/geo.json`;
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
const geos = JSON.parse(rawData) as Geo;
|
||||
|
||||
const sso = new SelectOptionsService();
|
||||
//Broker
|
||||
filePath = `./data/broker.json`;
|
||||
let data: string = readFileSync(filePath, 'utf8');
|
||||
const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten
|
||||
const generatedUserData = [];
|
||||
console.log(usersData.length);
|
||||
let i = 0,
|
||||
male = 0,
|
||||
female = 0;
|
||||
const targetPathProfile = `./pictures/profile`;
|
||||
deleteFilesOfDir(targetPathProfile);
|
||||
const targetPathLogo = `./pictures/logo`;
|
||||
deleteFilesOfDir(targetPathLogo);
|
||||
const targetPathProperty = `./pictures/property`;
|
||||
deleteFilesOfDir(targetPathProperty);
|
||||
fs.ensureDirSync(`./pictures/logo`);
|
||||
fs.ensureDirSync(`./pictures/profile`);
|
||||
fs.ensureDirSync(`./pictures/property`);
|
||||
|
||||
//User
|
||||
for (let index = 0; index < usersData.length; index++) {
|
||||
const userData = usersData[index];
|
||||
const user: User = createDefaultUser('', '', '');
|
||||
user.licensedIn = [];
|
||||
userData.licensedIn.forEach(l => {
|
||||
console.log(l['value'], l['name']);
|
||||
user.licensedIn.push({ registerNo: l['value'], state: l['name'] });
|
||||
// const openai = new OpenAI({
|
||||
// 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 pool = new Pool({connectionString})
|
||||
const client = new Pool({ connectionString });
|
||||
const db = drizzle(client, { schema, logger: true });
|
||||
const logger = winston.createLogger({
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
user.areasServed = [];
|
||||
user.areasServed = userData.areasServed.map(l => {
|
||||
return { county: l.split(',')[0].trim(), state: l.split(',')[1].trim() };
|
||||
});
|
||||
user.hasCompanyLogo = true;
|
||||
user.hasProfile = true;
|
||||
user.firstname = userData.firstname;
|
||||
user.lastname = userData.lastname;
|
||||
user.email = userData.email;
|
||||
user.phoneNumber = userData.phoneNumber;
|
||||
user.description = userData.description;
|
||||
user.companyName = userData.companyName;
|
||||
user.companyOverview = userData.companyOverview;
|
||||
user.companyWebsite = userData.companyWebsite;
|
||||
const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
|
||||
user.companyLocation = {};
|
||||
user.companyLocation.city = city;
|
||||
user.companyLocation.state = state;
|
||||
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||
user.companyLocation.latitude = cityGeo.latitude;
|
||||
user.companyLocation.longitude = cityGeo.longitude;
|
||||
user.offeredServices = userData.offeredServices;
|
||||
user.gender = userData.gender;
|
||||
user.customerType = 'professional';
|
||||
user.customerSubType = 'broker';
|
||||
user.created = new Date();
|
||||
user.updated = new Date();
|
||||
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 = `./src/assets/geo.json`;
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
const geos = JSON.parse(rawData) as Geo;
|
||||
|
||||
const u = await db
|
||||
.insert(schema.users)
|
||||
.values(convertUserToDrizzleUser(user))
|
||||
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname });
|
||||
generatedUserData.push(u[0]);
|
||||
i++;
|
||||
logger.info(`user_${index} inserted`);
|
||||
if (u[0].gender === 'male') {
|
||||
male++;
|
||||
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
||||
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||
} else {
|
||||
female++;
|
||||
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
|
||||
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||
const sso = new SelectOptionsService();
|
||||
//Broker
|
||||
filePath = `./data/broker.json`;
|
||||
let data: string = readFileSync(filePath, 'utf8');
|
||||
const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten
|
||||
const generatedUserData = [];
|
||||
console.log(usersData.length);
|
||||
let i = 0,
|
||||
male = 0,
|
||||
female = 0;
|
||||
const targetPathProfile = `./pictures/profile`;
|
||||
deleteFilesOfDir(targetPathProfile);
|
||||
const targetPathLogo = `./pictures/logo`;
|
||||
deleteFilesOfDir(targetPathLogo);
|
||||
const targetPathProperty = `./pictures/property`;
|
||||
deleteFilesOfDir(targetPathProperty);
|
||||
fs.ensureDirSync(`./pictures/logo`);
|
||||
fs.ensureDirSync(`./pictures/profile`);
|
||||
fs.ensureDirSync(`./pictures/property`);
|
||||
|
||||
//User
|
||||
for (let index = 0; index < usersData.length; index++) {
|
||||
const userData = usersData[index];
|
||||
const user: User = createDefaultUser('', '', '', null);
|
||||
user.licensedIn = [];
|
||||
userData.licensedIn.forEach(l => {
|
||||
console.log(l['value'], l['name']);
|
||||
user.licensedIn.push({ registerNo: l['value'], state: l['name'] });
|
||||
});
|
||||
user.areasServed = [];
|
||||
user.areasServed = userData.areasServed.map(l => {
|
||||
return { county: l.split(',')[0].trim(), state: l.split(',')[1].trim() };
|
||||
});
|
||||
user.hasCompanyLogo = true;
|
||||
user.hasProfile = true;
|
||||
user.firstname = userData.firstname;
|
||||
user.lastname = userData.lastname;
|
||||
user.email = userData.email;
|
||||
user.phoneNumber = userData.phoneNumber;
|
||||
user.description = userData.description;
|
||||
user.companyName = userData.companyName;
|
||||
user.companyOverview = userData.companyOverview;
|
||||
user.companyWebsite = userData.companyWebsite;
|
||||
const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
|
||||
user.location = {};
|
||||
user.location.name = city;
|
||||
user.location.state = state;
|
||||
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||
user.location.latitude = cityGeo.latitude;
|
||||
user.location.longitude = cityGeo.longitude;
|
||||
user.offeredServices = userData.offeredServices;
|
||||
user.gender = userData.gender;
|
||||
user.customerType = 'professional';
|
||||
user.customerSubType = 'broker';
|
||||
user.created = new Date();
|
||||
user.updated = new Date();
|
||||
|
||||
// const u = await db
|
||||
// .insert(schema.users)
|
||||
// .values(convertUserToDrizzleUser(user))
|
||||
// .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);
|
||||
i++;
|
||||
logger.info(`user_${index} inserted`);
|
||||
if (u.gender === 'male') {
|
||||
male++;
|
||||
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
||||
await storeProfilePicture(data, emailToDirName(u.email));
|
||||
} else {
|
||||
female++;
|
||||
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
|
||||
await storeProfilePicture(data, emailToDirName(u.email));
|
||||
}
|
||||
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
|
||||
await storeCompanyLogo(data, emailToDirName(u.email));
|
||||
}
|
||||
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
|
||||
await storeCompanyLogo(data, emailToDirName(u[0].email));
|
||||
}
|
||||
|
||||
//Corporate Listings
|
||||
filePath = `./data/commercials.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < commercialJsonData.length; index++) {
|
||||
const user = getRandomItem(generatedUserData);
|
||||
const commercial = createDefaultCommercialPropertyListing();
|
||||
const id = commercialJsonData[index].id;
|
||||
delete commercial.id;
|
||||
//Corporate Listings
|
||||
filePath = `./data/commercials.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < commercialJsonData.length; index++) {
|
||||
const user = getRandomItem(generatedUserData);
|
||||
const commercial = createDefaultCommercialPropertyListing();
|
||||
const id = commercialJsonData[index].id;
|
||||
delete commercial.id;
|
||||
|
||||
commercial.email = user.email;
|
||||
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercialJsonData[index].type)).value;
|
||||
commercial.title = commercialJsonData[index].title;
|
||||
commercial.description = commercialJsonData[index].description;
|
||||
try {
|
||||
const cityGeo = geos.states.find(s => s.state_code === commercialJsonData[index].state).cities.find(c => c.name === commercialJsonData[index].city);
|
||||
commercial.location = {};
|
||||
commercial.location.latitude = cityGeo.latitude;
|
||||
commercial.location.longitude = cityGeo.longitude;
|
||||
commercial.location.city = commercialJsonData[index].city;
|
||||
commercial.location.state = commercialJsonData[index].state;
|
||||
// console.log(JSON.stringify(commercial.location));
|
||||
} catch (e) {
|
||||
console.log(`----------------> ERROR ${commercialJsonData[index].state} - ${commercialJsonData[index].city}`);
|
||||
continue;
|
||||
commercial.email = user.email;
|
||||
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercialJsonData[index].type)).value;
|
||||
commercial.title = commercialJsonData[index].title;
|
||||
commercial.description = commercialJsonData[index].description;
|
||||
try {
|
||||
const cityGeo = geos.states.find(s => s.state_code === commercialJsonData[index].state).cities.find(c => c.name === commercialJsonData[index].city);
|
||||
commercial.location = {};
|
||||
commercial.location.latitude = cityGeo.latitude;
|
||||
commercial.location.longitude = cityGeo.longitude;
|
||||
commercial.location.name = commercialJsonData[index].city;
|
||||
commercial.location.state = commercialJsonData[index].state;
|
||||
// console.log(JSON.stringify(commercial.location));
|
||||
} catch (e) {
|
||||
console.log(`----------------> ERROR ${commercialJsonData[index].state} - ${commercialJsonData[index].city}`);
|
||||
continue;
|
||||
}
|
||||
commercial.price = commercialJsonData[index].price;
|
||||
commercial.listingsCategory = 'commercialProperty';
|
||||
commercial.draft = false;
|
||||
commercial.imageOrder = getFilenames(id);
|
||||
commercial.imagePath = emailToDirName(user.email);
|
||||
const insertionDate = getRandomDateWithinLastYear();
|
||||
commercial.created = insertionDate;
|
||||
commercial.updated = insertionDate;
|
||||
|
||||
const result = await commService.createListing(commercial); //await db.insert(schema.commercials).values(commercial).returning();
|
||||
try {
|
||||
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result.imagePath}/${result.serialId}`);
|
||||
} catch (err) {
|
||||
console.log(`----- No pictures available for ${id} ------ ${err}`);
|
||||
}
|
||||
}
|
||||
commercial.price = commercialJsonData[index].price;
|
||||
commercial.listingsCategory = 'commercialProperty';
|
||||
commercial.draft = false;
|
||||
commercial.imageOrder = getFilenames(id);
|
||||
commercial.imagePath = emailToDirName(user.email);
|
||||
const insertionDate = getRandomDateWithinLastYear();
|
||||
commercial.created = insertionDate;
|
||||
commercial.updated = insertionDate;
|
||||
|
||||
const result = await commService.createListing(commercial); //await db.insert(schema.commercials).values(commercial).returning();
|
||||
try {
|
||||
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result.imagePath}/${result.serialId}`);
|
||||
} catch (err) {
|
||||
console.log(`----- No pictures available for ${id} ------ ${err}`);
|
||||
//Business Listings
|
||||
filePath = `./data/businesses.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < businessJsonData.length; index++) {
|
||||
const business = createDefaultBusinessListing(); //businessJsonData[index];
|
||||
delete business.id;
|
||||
const user = getRandomItem(generatedUserData);
|
||||
business.email = user.email;
|
||||
business.type = sso.typesOfBusiness.find(e => e.oldValue === String(businessJsonData[index].type)).value;
|
||||
business.title = businessJsonData[index].title;
|
||||
business.description = businessJsonData[index].description;
|
||||
try {
|
||||
const cityGeo = geos.states.find(s => s.state_code === businessJsonData[index].state).cities.find(c => c.name === businessJsonData[index].city);
|
||||
business.location = {};
|
||||
business.location.latitude = cityGeo.latitude;
|
||||
business.location.longitude = cityGeo.longitude;
|
||||
business.location.name = businessJsonData[index].city;
|
||||
business.location.state = businessJsonData[index].state;
|
||||
} catch (e) {
|
||||
console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`);
|
||||
continue;
|
||||
}
|
||||
business.price = businessJsonData[index].price;
|
||||
business.title = businessJsonData[index].title;
|
||||
business.draft = businessJsonData[index].draft;
|
||||
business.listingsCategory = 'business';
|
||||
business.realEstateIncluded = businessJsonData[index].realEstateIncluded;
|
||||
business.leasedLocation = businessJsonData[index].leasedLocation;
|
||||
business.franchiseResale = businessJsonData[index].franchiseResale;
|
||||
|
||||
business.salesRevenue = businessJsonData[index].salesRevenue;
|
||||
business.cashFlow = businessJsonData[index].cashFlow;
|
||||
business.supportAndTraining = businessJsonData[index].supportAndTraining;
|
||||
business.employees = businessJsonData[index].employees;
|
||||
business.established = businessJsonData[index].established;
|
||||
business.internalListingNumber = businessJsonData[index].internalListingNumber;
|
||||
business.reasonForSale = businessJsonData[index].reasonForSale;
|
||||
business.brokerLicencing = businessJsonData[index].brokerLicencing;
|
||||
business.internals = businessJsonData[index].internals;
|
||||
business.imageName = emailToDirName(user.email);
|
||||
business.created = new Date(businessJsonData[index].created);
|
||||
business.updated = new Date(businessJsonData[index].created);
|
||||
|
||||
await businessService.createListing(business); //db.insert(schema.businesses).values(business);
|
||||
}
|
||||
}
|
||||
|
||||
//Business Listings
|
||||
filePath = `./data/businesses.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < businessJsonData.length; index++) {
|
||||
const business = createDefaultBusinessListing(); //businessJsonData[index];
|
||||
delete business.id;
|
||||
const user = getRandomItem(generatedUserData);
|
||||
business.email = user.email;
|
||||
business.type = sso.typesOfBusiness.find(e => e.oldValue === String(businessJsonData[index].type)).value;
|
||||
business.title = businessJsonData[index].title;
|
||||
business.description = businessJsonData[index].description;
|
||||
try {
|
||||
const cityGeo = geos.states.find(s => s.state_code === businessJsonData[index].state).cities.find(c => c.name === businessJsonData[index].city);
|
||||
business.location = {};
|
||||
business.location.latitude = cityGeo.latitude;
|
||||
business.location.longitude = cityGeo.longitude;
|
||||
business.location.city = businessJsonData[index].city;
|
||||
business.location.state = businessJsonData[index].state;
|
||||
} catch (e) {
|
||||
console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`);
|
||||
continue;
|
||||
}
|
||||
business.price = businessJsonData[index].price;
|
||||
business.title = businessJsonData[index].title;
|
||||
business.draft = businessJsonData[index].draft;
|
||||
business.listingsCategory = 'business';
|
||||
business.realEstateIncluded = businessJsonData[index].realEstateIncluded;
|
||||
business.leasedLocation = businessJsonData[index].leasedLocation;
|
||||
business.franchiseResale = businessJsonData[index].franchiseResale;
|
||||
|
||||
business.salesRevenue = businessJsonData[index].salesRevenue;
|
||||
business.cashFlow = businessJsonData[index].cashFlow;
|
||||
business.supportAndTraining = businessJsonData[index].supportAndTraining;
|
||||
business.employees = businessJsonData[index].employees;
|
||||
business.established = businessJsonData[index].established;
|
||||
business.internalListingNumber = businessJsonData[index].internalListingNumber;
|
||||
business.reasonForSale = businessJsonData[index].reasonForSale;
|
||||
business.brokerLicencing = businessJsonData[index].brokerLicencing;
|
||||
business.internals = businessJsonData[index].internals;
|
||||
business.imageName = emailToDirName(user.email);
|
||||
business.created = new Date(businessJsonData[index].created);
|
||||
business.updated = new Date(businessJsonData[index].created);
|
||||
|
||||
await businessService.createListing(business); //db.insert(schema.businesses).values(business);
|
||||
}
|
||||
|
||||
//End
|
||||
await client.end();
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
async function createEmbedding(text: string): Promise<number[]> {
|
||||
const response = await openai.embeddings.create({
|
||||
model: 'text-embedding-3-small',
|
||||
input: text,
|
||||
});
|
||||
return response.data[0].embedding;
|
||||
}
|
||||
//End
|
||||
await client.end();
|
||||
})();
|
||||
// function sleep(ms) {
|
||||
// return new Promise(resolve => setTimeout(resolve, ms));
|
||||
// }
|
||||
// async function createEmbedding(text: string): Promise<number[]> {
|
||||
// const response = await openai.embeddings.create({
|
||||
// model: 'text-embedding-3-small',
|
||||
// input: text,
|
||||
// });
|
||||
// return response.data[0].embedding;
|
||||
// }
|
||||
|
||||
function getRandomItem<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
|
|
@ -283,7 +284,7 @@ function getRandomItem<T>(arr: T[]): T {
|
|||
}
|
||||
function getFilenames(id: string): string[] {
|
||||
try {
|
||||
let filePath = `./pictures_base/property/${id}`;
|
||||
const filePath = `./pictures_base/property/${id}`;
|
||||
return readdirSync(filePath);
|
||||
} catch (e) {
|
||||
return [];
|
||||
|
|
@ -300,7 +301,7 @@ function getRandomDateWithinLastYear(): Date {
|
|||
return randomDate;
|
||||
}
|
||||
async function storeProfilePicture(buffer: Buffer, userId: string) {
|
||||
let quality = 50;
|
||||
const quality = 50;
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
|
|
@ -310,7 +311,7 @@ async function storeProfilePicture(buffer: Buffer, userId: string) {
|
|||
}
|
||||
|
||||
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const quality = 50;
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { readFileSync } from 'fs';
|
||||
import { Pool } from 'pg';
|
||||
import { BusinessListingService } from 'src/listings/business-listing.service';
|
||||
import { CommercialPropertyService } from 'src/listings/commercial-property.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import winston from 'winston';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from '../models/db.model';
|
||||
import * as schema from './schema';
|
||||
|
||||
(async () => {
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const client = new Pool({ connectionString });
|
||||
const db = drizzle(client, { schema, logger: true });
|
||||
const logger = winston.createLogger({
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
const commService = new CommercialPropertyService(null, db);
|
||||
const businessService = new BusinessListingService(null, db);
|
||||
const userService = new UserService(null, db, null, null);
|
||||
|
||||
//Delete Content
|
||||
await db.delete(schema.commercials);
|
||||
await db.delete(schema.businesses);
|
||||
await db.delete(schema.users);
|
||||
|
||||
let filePath = `./data/users_export.json`;
|
||||
let data: string = readFileSync(filePath, 'utf8');
|
||||
const usersData: User[] = JSON.parse(data); // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < usersData.length; index++) {
|
||||
const user = usersData[index];
|
||||
delete user.id;
|
||||
const u = await userService.saveUser(user, false);
|
||||
logger.info(`user_${index} inserted`);
|
||||
}
|
||||
|
||||
//Corporate Listings
|
||||
filePath = `./data/commercials_export.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < commercialJsonData.length; index++) {
|
||||
const commercial = commercialJsonData[index];
|
||||
delete commercial.id;
|
||||
const result = await commService.createListing(commercial);
|
||||
}
|
||||
|
||||
//Business Listings
|
||||
filePath = `./data/businesses_export.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < businessJsonData.length; index++) {
|
||||
const business = businessJsonData[index];
|
||||
delete business.id;
|
||||
await businessService.createListing(business);
|
||||
}
|
||||
|
||||
//End
|
||||
await client.end();
|
||||
})();
|
||||
function getRandomItem<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('The array is empty.');
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
return arr[randomIndex];
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pkg from 'pg';
|
||||
import * as schema from './schema.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();
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
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 $$;
|
||||
|
|
@ -1,541 +0,0 @@
|
|||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1723045357281,
|
||||
"tag": "0000_lean_marvex",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,92 +1,171 @@
|
|||
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
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';
|
||||
export const PG_CONNECTION = 'PG_CONNECTION';
|
||||
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
||||
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']);
|
||||
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'seller', 'professional']);
|
||||
export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
|
||||
export const subscriptionTypeEnum = pgEnum('subscriptionType', ['free', 'professional', 'broker']);
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||
firstname: varchar('firstname', { length: 255 }).notNull(),
|
||||
lastname: varchar('lastname', { length: 255 }).notNull(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
||||
description: text('description'),
|
||||
companyName: varchar('companyName', { length: 255 }),
|
||||
companyOverview: text('companyOverview'),
|
||||
companyWebsite: varchar('companyWebsite', { length: 255 }),
|
||||
city: varchar('city', { length: 255 }),
|
||||
state: char('state', { length: 2 }),
|
||||
offeredServices: text('offeredServices'),
|
||||
areasServed: jsonb('areasServed').$type<AreasServed[]>(),
|
||||
hasProfile: boolean('hasProfile'),
|
||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||
gender: genderEnum('gender'),
|
||||
customerType: customerTypeEnum('customerType'),
|
||||
customerSubType: customerSubTypeEnum('customerSubType'),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
latitude: doublePrecision('latitude'),
|
||||
longitude: doublePrecision('longitude'),
|
||||
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||
});
|
||||
// Neue JSONB-basierte Tabellen
|
||||
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 = pgTable('businesses', {
|
||||
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||
type: varchar('type', { length: 255 }),
|
||||
title: varchar('title', { length: 255 }),
|
||||
description: text('description'),
|
||||
city: varchar('city', { length: 255 }),
|
||||
state: char('state', { length: 2 }),
|
||||
// zipCode: integer('zipCode'),
|
||||
// county: varchar('county', { length: 255 }),
|
||||
price: doublePrecision('price'),
|
||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||
draft: boolean('draft'),
|
||||
listingsCategory: listingsCategoryEnum('listingsCategory'), //varchar('listingsCategory', { length: 255 }),
|
||||
realEstateIncluded: boolean('realEstateIncluded'),
|
||||
leasedLocation: boolean('leasedLocation'),
|
||||
franchiseResale: boolean('franchiseResale'),
|
||||
salesRevenue: doublePrecision('salesRevenue'),
|
||||
cashFlow: doublePrecision('cashFlow'),
|
||||
supportAndTraining: text('supportAndTraining'),
|
||||
employees: integer('employees'),
|
||||
established: integer('established'),
|
||||
internalListingNumber: integer('internalListingNumber'),
|
||||
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
||||
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
||||
internals: text('internals'),
|
||||
imageName: varchar('imageName', { length: 200 }),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
latitude: doublePrecision('latitude'),
|
||||
longitude: doublePrecision('longitude'),
|
||||
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||
});
|
||||
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 = pgTable('commercials', {
|
||||
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(),
|
||||
firstname: varchar('firstname', { length: 255 }).notNull(),
|
||||
lastname: varchar('lastname', { length: 255 }).notNull(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
||||
description: text('description'),
|
||||
companyName: varchar('companyName', { length: 255 }),
|
||||
companyOverview: text('companyOverview'),
|
||||
companyWebsite: varchar('companyWebsite', { length: 255 }),
|
||||
offeredServices: text('offeredServices'),
|
||||
areasServed: jsonb('areasServed').$type<AreasServed[]>(),
|
||||
hasProfile: boolean('hasProfile'),
|
||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||
gender: genderEnum('gender'),
|
||||
customerType: customerTypeEnum('customerType'),
|
||||
customerSubType: customerSubTypeEnum('customerSubType'),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
subscriptionId: text('subscriptionId'),
|
||||
subscriptionPlan: subscriptionTypeEnum('subscriptionPlan'),
|
||||
location: jsonb('location'),
|
||||
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(
|
||||
'businesses',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||
type: varchar('type', { length: 255 }),
|
||||
title: varchar('title', { length: 255 }),
|
||||
description: text('description'),
|
||||
price: doublePrecision('price'),
|
||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||
draft: boolean('draft'),
|
||||
listingsCategory: listingsCategoryEnum('listingsCategory'),
|
||||
realEstateIncluded: boolean('realEstateIncluded'),
|
||||
leasedLocation: boolean('leasedLocation'),
|
||||
franchiseResale: boolean('franchiseResale'),
|
||||
salesRevenue: doublePrecision('salesRevenue'),
|
||||
cashFlow: doublePrecision('cashFlow'),
|
||||
supportAndTraining: text('supportAndTraining'),
|
||||
employees: integer('employees'),
|
||||
established: integer('established'),
|
||||
internalListingNumber: integer('internalListingNumber'),
|
||||
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
||||
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
||||
internals: text('internals'),
|
||||
imageName: varchar('imageName', { length: 200 }),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
location: jsonb('location'),
|
||||
},
|
||||
table => ({
|
||||
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(
|
||||
'commercials',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||
serialId: serial('serialId'),
|
||||
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||
type: varchar('type', { length: 255 }),
|
||||
title: varchar('title', { length: 255 }),
|
||||
description: text('description'),
|
||||
price: doublePrecision('price'),
|
||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||
listingsCategory: listingsCategoryEnum('listingsCategory'),
|
||||
draft: boolean('draft'),
|
||||
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
||||
imagePath: varchar('imagePath', { length: 200 }),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
location: jsonb('location'),
|
||||
},
|
||||
table => ({
|
||||
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(),
|
||||
serialId: serial('serialId'),
|
||||
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||
type: varchar('type', { length: 255 }),
|
||||
title: varchar('title', { length: 255 }),
|
||||
description: text('description'),
|
||||
city: varchar('city', { length: 255 }),
|
||||
state: char('state', { length: 2 }),
|
||||
price: doublePrecision('price'),
|
||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||
listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }),
|
||||
draft: boolean('draft'),
|
||||
// zipCode: integer('zipCode'),
|
||||
// county: varchar('county', { length: 255 }),
|
||||
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
||||
imagePath: varchar('imagePath', { length: 200 }),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
latitude: doublePrecision('latitude'),
|
||||
longitude: doublePrecision('longitude'),
|
||||
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||
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'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
|
||||
// Angenommen, du hast eine Datei `databaseModels.js` mit deinen pgTable-Definitionen
|
||||
const { users } = require('./schema.js');
|
||||
const { users } = require('./schema');
|
||||
|
||||
function generateTypeScriptInterface(tableDefinition, tableName) {
|
||||
let interfaceString = `export interface ${tableName} {\n`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
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' };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
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 {}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,22 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import path, { join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { Logger } from 'winston';
|
||||
import { ImageProperty, Subscription } from '../models/main.model.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class FileService {
|
||||
private subscriptions: any;
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
this.loadSubscriptions();
|
||||
fs.ensureDirSync(`./pictures`);
|
||||
fs.ensureDirSync(`./pictures/profile`);
|
||||
fs.ensureDirSync(`./pictures/logo`);
|
||||
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
|
||||
// ############
|
||||
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
|
|
@ -50,7 +31,7 @@ export class FileService {
|
|||
// Logo
|
||||
// ############
|
||||
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
|
|
@ -79,7 +60,6 @@ export class FileService {
|
|||
}
|
||||
}
|
||||
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
||||
const result: ImageProperty[] = [];
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
if (fs.existsSync(directory)) {
|
||||
const files = await fs.readdir(directory);
|
||||
|
|
@ -89,7 +69,6 @@ export class FileService {
|
|||
}
|
||||
}
|
||||
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
fs.ensureDirSync(`${directory}`);
|
||||
const imageName = await this.getNextImageName(directory);
|
||||
|
|
@ -116,16 +95,15 @@ export class FileService {
|
|||
}
|
||||
}
|
||||
async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) {
|
||||
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
||||
let output;
|
||||
let start = Date.now();
|
||||
output = await sharp(buffer)
|
||||
const quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
||||
const start = Date.now();
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 1500 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
|
||||
let timeTaken = Date.now() - start;
|
||||
const timeTaken = Date.now() - start;
|
||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
||||
}
|
||||
deleteImage(path: string) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: 'FIREBASE_ADMIN',
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const serviceAccount = {
|
||||
projectId: configService.get<string>('FIREBASE_PROJECT_ID'),
|
||||
clientEmail: configService.get<string>('FIREBASE_CLIENT_EMAIL'),
|
||||
privateKey: configService.get<string>('FIREBASE_PRIVATE_KEY')?.replace(/\\n/g, '\n'),
|
||||
};
|
||||
|
||||
if (!admin.apps.length) {
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
});
|
||||
}
|
||||
|
||||
return admin;
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: ['FIREBASE_ADMIN'],
|
||||
})
|
||||
export class FirebaseAdminModule {}
|
||||
|
|
@ -1,27 +1,41 @@
|
|||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { CountyRequest } from 'src/models/server.model.js';
|
||||
import { GeoService } from './geo.service.js';
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { RealIp } from 'src/decorators/real-ip.decorator';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { RealIpInfo } from 'src/models/main.model';
|
||||
import { CountyRequest } from 'src/models/server.model';
|
||||
import { GeoService } from './geo.service';
|
||||
|
||||
@Controller('geo')
|
||||
export class GeoController {
|
||||
constructor(private geoService: GeoService) {}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':prefix')
|
||||
findByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('citiesandstates/:prefix')
|
||||
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesAndStatesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':prefix/:state')
|
||||
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix, state);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('counties')
|
||||
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { GeoController } from './geo.controller.js';
|
||||
import { GeoService } from './geo.service.js';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { GeoController } from './geo.controller';
|
||||
import { GeoService } from './geo.service';
|
||||
|
||||
@Module({
|
||||
imports: [FirebaseAdminModule],
|
||||
controllers: [GeoController],
|
||||
providers: [GeoService]
|
||||
providers: [GeoService],
|
||||
})
|
||||
export class GeoModule {}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { CountyResult, GeoResult } from 'src/models/main.model.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { City, CountyData, Geo, State } from '../models/server.model.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { join } from 'path';
|
||||
import { CityAndStateResult, CountyResult, GeoResult, IpInfo, RealIpInfo } from 'src/models/main.model';
|
||||
import { Logger } from 'winston';
|
||||
import { City, CountyData, Geo, State } from '../models/server.model';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class GeoService {
|
||||
geo: Geo;
|
||||
counties: CountyData[];
|
||||
constructor() {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
this.loadGeo();
|
||||
}
|
||||
private loadGeo(): void {
|
||||
|
|
@ -24,13 +24,13 @@ export class GeoService {
|
|||
this.counties = JSON.parse(rawCountiesData);
|
||||
}
|
||||
findCountiesStartingWith(prefix: string, states?: string[]) {
|
||||
let results: CountyResult[] = [];
|
||||
const results: CountyResult[] = [];
|
||||
let idCounter = 1;
|
||||
|
||||
this.counties.forEach(stateData => {
|
||||
if (!states || states.includes(stateData.state)) {
|
||||
stateData.counties.forEach(county => {
|
||||
if (county.startsWith(prefix.toUpperCase())) {
|
||||
if (county.startsWith(prefix?.toUpperCase())) {
|
||||
results.push({
|
||||
id: idCounter++,
|
||||
name: county,
|
||||
|
|
@ -52,7 +52,7 @@ export class GeoService {
|
|||
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
result.push({
|
||||
id: city.id,
|
||||
city: city.name,
|
||||
name: city.name,
|
||||
state: state.state_code,
|
||||
//state_code: state.state_code,
|
||||
latitude: city.latitude,
|
||||
|
|
@ -63,8 +63,8 @@ export class GeoService {
|
|||
});
|
||||
return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result;
|
||||
}
|
||||
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> {
|
||||
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
|
||||
findCitiesAndStatesStartingWith(prefix: string): Array<CityAndStateResult> {
|
||||
const results: Array<CityAndStateResult> = [];
|
||||
|
||||
const lowercasePrefix = prefix.toLowerCase();
|
||||
|
||||
|
|
@ -73,10 +73,9 @@ export class GeoService {
|
|||
for (const state of this.geo.states) {
|
||||
if (state.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: state.id.toString(),
|
||||
name: state.name,
|
||||
id: state.id,
|
||||
type: 'state',
|
||||
state: state.state_code,
|
||||
content: state,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -84,10 +83,9 @@ export class GeoService {
|
|||
for (const city of state.cities) {
|
||||
if (city.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: city.id.toString(),
|
||||
name: city.name,
|
||||
id: city.id,
|
||||
type: 'city',
|
||||
state: state.state_code,
|
||||
content: { state: state.state_code, ...city },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -97,10 +95,27 @@ export class GeoService {
|
|||
return results.sort((a, b) => {
|
||||
if (a.type === 'state' && b.type === 'city') return -1;
|
||||
if (a.type === 'city' && b.type === 'state') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
return a.content.name.localeCompare(b.content.name);
|
||||
});
|
||||
}
|
||||
getCityWithCoords(state: string, city: string): 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { CommercialPropertyService } from '../listings/commercial-property.service.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { CommercialPropertyService } from '../listings/commercial-property.service';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||
|
||||
@Controller('image')
|
||||
export class ImageController {
|
||||
|
|
@ -17,12 +18,14 @@ export class ImageController {
|
|||
// ############
|
||||
// Property
|
||||
// ############
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('uploadPropertyPicture/:imagePath/:serial')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
|
||||
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
|
||||
await this.listingService.addImage(imagePath, serial, imagename);
|
||||
}
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
|
||||
|
|
@ -31,11 +34,13 @@ export class ImageController {
|
|||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('uploadProfile/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||
}
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('profile/:email/')
|
||||
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||
|
|
@ -43,11 +48,13 @@ export class ImageController {
|
|||
// ############
|
||||
// Logo
|
||||
// ############
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('uploadCompanyLogo/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||
}
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('logo/:email/')
|
||||
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { ListingsModule } from '../listings/listings.module.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { ImageController } from './image.controller.js';
|
||||
import { ImageService } from './image.service.js';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ListingsModule } from '../listings/listings.module';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||
import { ImageController } from './image.controller';
|
||||
import { ImageService } from './image.service';
|
||||
|
||||
@Module({
|
||||
imports: [ListingsModule],
|
||||
imports: [ListingsModule,FirebaseAdminModule],
|
||||
controllers: [ImageController],
|
||||
providers: [ImageService, FileService, SelectOptionsService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
// 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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('Missing or invalid authorization token');
|
||||
}
|
||||
|
||||
const token = authHeader.split('Bearer ')[1];
|
||||
|
||||
try {
|
||||
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
|
||||
|
||||
// Check if email is verified (optional but recommended)
|
||||
if (!decodedToken.email_verified) {
|
||||
throw new UnauthorizedException('Email not verified');
|
||||
}
|
||||
|
||||
// Add the user to the request
|
||||
request.user = {
|
||||
uid: decodedToken.uid,
|
||||
email: decodedToken.email,
|
||||
role: decodedToken.role || null,
|
||||
// Add other user info as needed
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
|
||||
canActivate(context: ExecutionContext) {
|
||||
// Add your custom authentication logic here
|
||||
// for example, call super.logIn(request) to establish a session.
|
||||
return super.canActivate(context);
|
||||
}
|
||||
handleRequest(err, user, info) {
|
||||
// You can throw an exception based on either "info" or "err" arguments
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException(info);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class LocalhostGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const ip = request.ip;
|
||||
|
||||
// Liste der erlaubten IPs
|
||||
const allowedIPs = ['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1'];
|
||||
|
||||
if (!allowedIPs.includes(ip)) {
|
||||
console.warn(`Versuchter Zugriff von unerlaubter IP: ${ip}`);
|
||||
throw new ForbiddenException('Dieser Endpunkt kann nur lokal aufgerufen werden');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
|
||||
import * as admin from 'firebase-admin';
|
||||
|
||||
@Injectable()
|
||||
export class OptionalAuthGuard implements CanActivate {
|
||||
constructor(@Inject('FIREBASE_ADMIN') private firebaseAdmin: admin.app.App) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
//throw new UnauthorizedException('Missing or invalid authorization token');
|
||||
return true;
|
||||
}
|
||||
|
||||
const token = authHeader.split('Bearer ')[1];
|
||||
|
||||
try {
|
||||
const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
|
||||
|
||||
// Check if email is verified (optional but recommended)
|
||||
if (!decodedToken.email_verified) {
|
||||
//throw new UnauthorizedException('Email not verified');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add the user to the request
|
||||
request.user = {
|
||||
uid: decodedToken.uid,
|
||||
email: decodedToken.email,
|
||||
role: decodedToken.role || null,
|
||||
// Add other user info as needed
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
//throw new UnauthorizedException('Invalid token');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
|
||||
// import * as admin from 'firebase-admin';
|
||||
|
||||
// @Injectable()
|
||||
// export class OptionalAuthGuard implements CanActivate {
|
||||
// constructor(
|
||||
// @Inject('FIREBASE_ADMIN')
|
||||
// private readonly firebaseAdmin: typeof admin,
|
||||
// ) {}
|
||||
|
||||
// async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// const request = context.switchToHttp().getRequest<Request>();
|
||||
// const token = this.extractTokenFromHeader(request);
|
||||
|
||||
// if (!token) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const decodedToken = await this.firebaseAdmin.auth().verifyIdToken(token);
|
||||
// request['user'] = decodedToken;
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// //throw new UnauthorizedException('Invalid token');
|
||||
// request['user'] = null;
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private extractTokenFromHeader(request: Request): string | undefined {
|
||||
// const [type, token] = request.headers['authorization']?.split(' ') ?? [];
|
||||
// return type === 'Bearer' ? token : undefined;
|
||||
// }
|
||||
// }
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
|
||||
handleRequest(err, user, info) {
|
||||
// Wenn der Benutzer nicht authentifiziert ist, aber kein Fehler vorliegt, geben Sie null zurück
|
||||
if (err || !user) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { Body, Controller, Inject, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { UserListingCriteria } from 'src/models/main.model.js';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { UserListingCriteria } from 'src/models/main.model';
|
||||
import { Logger } from 'winston';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
@Controller('listings/professionals_brokers')
|
||||
export class BrokerListingsController {
|
||||
|
|
@ -11,8 +12,9 @@ export class BrokerListingsController {
|
|||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('search')
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
return this.userService.searchUserListings(criteria);
|
||||
async find(@Body() criteria: UserListingCriteria): Promise<any> {
|
||||
return await this.userService.searchUserListings(criteria);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,141 +1,207 @@
|
|||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { and, arrayContains, asc, count, desc, eq, gte, inArray, lte, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { BusinessListing, BusinessListingSchema } from '../models/db.model.js';
|
||||
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery } from '../utils.js';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { businesses_json, PG_CONNECTION, users_json } from '../drizzle/schema';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { BusinessListing, BusinessListingSchema } from '../models/db.model';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { getDistanceQuery, splitName } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class BusinessListingService {
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService?: FileService,
|
||||
private geoService?: GeoService,
|
||||
) {}
|
||||
|
||||
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
|
||||
private getWhereConditions(criteria: BusinessListingCriteria, user: JwtUser): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
|
||||
whereConditions.push(sql`(${businesses_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`);
|
||||
}
|
||||
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
||||
whereConditions.push(sql`${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
whereConditions.push(inArray(businesses.type, criteria.types));
|
||||
whereConditions.push(inArray(sql`${businesses_json.data}->>'type'`, criteria.types));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(eq(businesses.state, criteria.state));
|
||||
whereConditions.push(sql`(${businesses_json.data}->'location'->>'state') = ${criteria.state}`);
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
whereConditions.push(gte(businesses.price, criteria.minPrice));
|
||||
whereConditions.push(gte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.minPrice));
|
||||
}
|
||||
|
||||
if (criteria.maxPrice) {
|
||||
whereConditions.push(lte(businesses.price, criteria.maxPrice));
|
||||
whereConditions.push(lte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.maxPrice));
|
||||
}
|
||||
|
||||
if (criteria.minRevenue) {
|
||||
whereConditions.push(gte(businesses.salesRevenue, criteria.minRevenue));
|
||||
whereConditions.push(gte(sql`(${businesses_json.data}->>'salesRevenue')::double precision`, criteria.minRevenue));
|
||||
}
|
||||
|
||||
if (criteria.maxRevenue) {
|
||||
whereConditions.push(lte(businesses.salesRevenue, criteria.maxRevenue));
|
||||
whereConditions.push(lte(sql`(${businesses_json.data}->>'salesRevenue')::double precision`, criteria.maxRevenue));
|
||||
}
|
||||
|
||||
if (criteria.minCashFlow) {
|
||||
whereConditions.push(gte(businesses.cashFlow, criteria.minCashFlow));
|
||||
whereConditions.push(gte(sql`(${businesses_json.data}->>'cashFlow')::double precision`, criteria.minCashFlow));
|
||||
}
|
||||
|
||||
if (criteria.maxCashFlow) {
|
||||
whereConditions.push(lte(businesses.cashFlow, criteria.maxCashFlow));
|
||||
whereConditions.push(lte(sql`(${businesses_json.data}->>'cashFlow')::double precision`, criteria.maxCashFlow));
|
||||
}
|
||||
|
||||
if (criteria.minNumberEmployees) {
|
||||
whereConditions.push(gte(businesses.employees, criteria.minNumberEmployees));
|
||||
whereConditions.push(gte(sql`(${businesses_json.data}->>'employees')::integer`, criteria.minNumberEmployees));
|
||||
}
|
||||
|
||||
if (criteria.maxNumberEmployees) {
|
||||
whereConditions.push(lte(businesses.employees, criteria.maxNumberEmployees));
|
||||
whereConditions.push(lte(sql`(${businesses_json.data}->>'employees')::integer`, criteria.maxNumberEmployees));
|
||||
}
|
||||
|
||||
if (criteria.establishedSince) {
|
||||
whereConditions.push(gte(businesses.established, criteria.establishedSince));
|
||||
}
|
||||
|
||||
if (criteria.establishedUntil) {
|
||||
whereConditions.push(lte(businesses.established, criteria.establishedUntil));
|
||||
if (criteria.establishedMin) {
|
||||
whereConditions.push(gte(sql`(${businesses_json.data}->>'established')::integer`, criteria.establishedMin));
|
||||
}
|
||||
|
||||
if (criteria.realEstateChecked) {
|
||||
whereConditions.push(eq(businesses.realEstateIncluded, criteria.realEstateChecked));
|
||||
whereConditions.push(eq(sql`(${businesses_json.data}->>'realEstateIncluded')::boolean`, criteria.realEstateChecked));
|
||||
}
|
||||
|
||||
if (criteria.leasedLocation) {
|
||||
whereConditions.push(eq(businesses.leasedLocation, criteria.leasedLocation));
|
||||
whereConditions.push(eq(sql`(${businesses_json.data}->>'leasedLocation')::boolean`, criteria.leasedLocation));
|
||||
}
|
||||
|
||||
if (criteria.franchiseResale) {
|
||||
whereConditions.push(eq(businesses.franchiseResale, criteria.franchiseResale));
|
||||
whereConditions.push(eq(sql`(${businesses_json.data}->>'franchiseResale')::boolean`, criteria.franchiseResale));
|
||||
}
|
||||
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`)));
|
||||
whereConditions.push(sql`(${businesses_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${businesses_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`);
|
||||
}
|
||||
|
||||
if (criteria.brokerName) {
|
||||
whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${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}%`}`);
|
||||
}
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
|
||||
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;
|
||||
}
|
||||
|
||||
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn
|
||||
.select({
|
||||
business: businesses,
|
||||
brokerFirstName: schema.users.firstname,
|
||||
brokerLastName: schema.users.lastname,
|
||||
business: businesses_json,
|
||||
brokerFirstName: sql`${users_json.data}->>'firstname'`.as('brokerFirstName'),
|
||||
brokerLastName: sql`${users_json.data}->>'lastname'`.as('brokerLastName'),
|
||||
})
|
||||
.from(businesses)
|
||||
.leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
||||
.from(businesses_json)
|
||||
.leftJoin(users_json, eq(businesses_json.email, users_json.email));
|
||||
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
const whereConditions = this.getWhereConditions(criteria, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
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
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const data = await query;
|
||||
const totalCount = await this.getBusinessListingsCount(criteria);
|
||||
const results = data.map(r => r.business).map(r => convertDrizzleBusinessToBusiness(r));
|
||||
const totalCount = await this.getBusinessListingsCount(criteria, user);
|
||||
const results = data.map(r => ({
|
||||
id: r.business.id,
|
||||
email: r.business.email,
|
||||
...(r.business.data as BusinessListing),
|
||||
brokerFirstName: r.brokerFirstName,
|
||||
brokerLastName: r.brokerLastName,
|
||||
}));
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
async getBusinessListingsCount(criteria: BusinessListingCriteria): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(businesses).leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
||||
async getBusinessListingsCount(criteria: BusinessListingCriteria, user: JwtUser): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(businesses_json).leftJoin(users_json, eq(businesses_json.email, users_json.email));
|
||||
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
const whereConditions = this.getWhereConditions(criteria, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
|
|
@ -147,80 +213,107 @@ export class BusinessListingService {
|
|||
}
|
||||
|
||||
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
|
||||
let result = await this.conn
|
||||
const conditions = [];
|
||||
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()
|
||||
.from(businesses)
|
||||
.where(and(sql`${businesses.id} = ${id}`));
|
||||
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||
return convertDrizzleBusinessToBusiness(result[0]) as BusinessListing;
|
||||
.from(businesses_json)
|
||||
.where(and(...conditions));
|
||||
if (result.length > 0) {
|
||||
return { id: result[0].id, email: result[0].email, ...(result[0].data as BusinessListing) } as BusinessListing;
|
||||
} else {
|
||||
throw new BadRequestException(`No entry available for ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(businesses.imageName, emailToDirName(email)));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
conditions.push(ne(businesses.draft, true));
|
||||
conditions.push(eq(businesses_json.email, email));
|
||||
if (email !== user?.email && user?.role !== 'admin') {
|
||||
conditions.push(sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`);
|
||||
}
|
||||
const listings = (await this.conn
|
||||
const listings = await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.where(and(...conditions))) as BusinessListing[];
|
||||
|
||||
return listings.map(l => convertDrizzleBusinessToBusiness(l));
|
||||
.from(businesses_json)
|
||||
.where(and(...conditions));
|
||||
return listings.map(l => ({ id: l.id, email: l.email, ...(l.data as BusinessListing) }) as BusinessListing);
|
||||
}
|
||||
|
||||
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> {
|
||||
try {
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
data.updated = new Date();
|
||||
const validatedBusinessListing = BusinessListingSchema.parse(data);
|
||||
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
|
||||
const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
|
||||
return convertDrizzleBusinessToBusiness(createdListing);
|
||||
BusinessListingSchema.parse(data);
|
||||
const { id, email, ...rest } = data;
|
||||
const convertedBusinessListing = { email, data: rest };
|
||||
const [createdListing] = await this.conn.insert(businesses_json).values(convertedBusinessListing).returning();
|
||||
return { id: createdListing.id, email: createdListing.email, ...(createdListing.data as BusinessListing) };
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
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;
|
||||
}
|
||||
}
|
||||
// #### UPDATE Business ########################################
|
||||
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> {
|
||||
|
||||
async updateBusinessListing(id: string, data: BusinessListing, user: JwtUser): Promise<BusinessListing> {
|
||||
try {
|
||||
const [existingListing] = await this.conn.select().from(businesses_json).where(eq(businesses_json.id, id));
|
||||
|
||||
if (!existingListing) {
|
||||
throw new NotFoundException(`Business listing with id ${id} not found`);
|
||||
}
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
const validatedBusinessListing = BusinessListingSchema.parse(data);
|
||||
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
|
||||
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
|
||||
return convertDrizzleBusinessToBusiness(updateListing);
|
||||
if (existingListing.email === user?.email) {
|
||||
data.favoritesForUser = (<BusinessListing>existingListing.data).favoritesForUser || [];
|
||||
}
|
||||
BusinessListingSchema.parse(data);
|
||||
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) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
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;
|
||||
}
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
await this.conn.delete(businesses).where(eq(businesses.id, id));
|
||||
await this.conn.delete(businesses_json).where(eq(businesses_json.id, id));
|
||||
}
|
||||
// ##############################################################
|
||||
// States
|
||||
// ##############################################################
|
||||
async getStates(): Promise<any[]> {
|
||||
return await this.conn
|
||||
.select({ state: businesses.state, count: sql<number>`count(${businesses.id})`.mapWith(Number) })
|
||||
.from(businesses)
|
||||
.groupBy(sql`${businesses.state}`)
|
||||
.orderBy(sql`count desc`);
|
||||
|
||||
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
|
||||
await this.conn
|
||||
.update(businesses_json)
|
||||
.set({
|
||||
data: sql`jsonb_set(${businesses_json.data}, '{favoritesForUser}', array_remove((${businesses_json.data}->>'favoritesForUser')::jsonb, ${user.email}))`,
|
||||
})
|
||||
.where(eq(businesses_json.id, id));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { BusinessListing } from 'src/models/db.model.js';
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { BusinessListingService } from './business-listing.service.js';
|
||||
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { BusinessListing } from '../models/db.model';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
|
||||
@Controller('listings/business')
|
||||
export class BusinessListingsController {
|
||||
|
|
@ -13,49 +15,54 @@ export class BusinessListingsController {
|
|||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
return await this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('favorites/all')
|
||||
async findFavorites(@Request() req): Promise<any> {
|
||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('user/:userid')
|
||||
findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||
async findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
return await this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('find')
|
||||
find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
async find(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: BusinessListingCriteria): Promise<number> {
|
||||
return this.listingsService.getBusinessListingsCount(criteria);
|
||||
async findTotal(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<number> {
|
||||
return await this.listingsService.getBusinessListingsCount(criteria, req.user as JwtUser);
|
||||
}
|
||||
// @UseGuards(OptionalJwtAuthGuard)
|
||||
// @Post('search')
|
||||
// search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
// return this.listingsService.searchBusinessListings(criteria.prompt);
|
||||
// }
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return this.listingsService.createListing(listing);
|
||||
async create(@Body() listing: any) {
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Put()
|
||||
update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return this.listingsService.updateBusinessListing(listing.id, listing);
|
||||
async update(@Request() req, @Body() listing: any) {
|
||||
return await this.listingsService.updateBusinessListing(listing.id, listing, req.user as JwtUser);
|
||||
}
|
||||
@Delete(':id')
|
||||
deleteById(@Param('id') id: string) {
|
||||
this.listingsService.deleteListing(id);
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Delete('listing/:id')
|
||||
async deleteById(@Param('id') id: string) {
|
||||
await this.listingsService.deleteListing(id);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('favorite/:id')
|
||||
async deleteFavorite(@Request() req, @Param('id') id: string) {
|
||||
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { FileService } from '../file/file.service';
|
||||
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { CommercialPropertyListing } from '../models/db.model';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { CommercialPropertyService } from './commercial-property.service.js';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
|
||||
@Controller('listings/commercialProperty')
|
||||
export class CommercialPropertyListingsController {
|
||||
|
|
@ -15,43 +17,56 @@ export class CommercialPropertyListingsController {
|
|||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
return await this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('user/:email')
|
||||
findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('favorites/all')
|
||||
async findFavorites(@Request() req): Promise<any> {
|
||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get('user/:email')
|
||||
async findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||
return await this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('find')
|
||||
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
return this.listingsService.getCommercialPropertiesCount(criteria);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
async findTotal(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
return await this.listingsService.getCommercialPropertiesCount(criteria, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
async create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Put()
|
||||
async update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||
async update(@Request() req, @Body() listing: any) {
|
||||
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing, req.user as JwtUser);
|
||||
}
|
||||
@Delete(':id/:imagePath')
|
||||
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
this.listingsService.deleteListing(id);
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Delete('listing/:id/:imagePath')
|
||||
async deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
await this.listingsService.deleteListing(id);
|
||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||
}
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete('favorite/:id')
|
||||
async deleteFavorite(@Request() req, @Param('id') id: string) {
|
||||
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { and, arrayContains, asc, count, desc, eq, gte, inArray, lte, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js';
|
||||
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
import { convertCommercialToDrizzleCommercial, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils.js';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { commercials_json, PG_CONNECTION } from '../drizzle/schema';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { getDistanceQuery } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class CommercialPropertyService {
|
||||
|
|
@ -20,65 +20,86 @@ export class CommercialPropertyService {
|
|||
private fileService?: FileService,
|
||||
private geoService?: GeoService,
|
||||
) {}
|
||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
|
||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria, user: JwtUser): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
|
||||
whereConditions.push(sql`(${commercials_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`);
|
||||
}
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
||||
whereConditions.push(sql`${getDistanceQuery(commercials_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
whereConditions.push(inArray(schema.commercials.type, criteria.types));
|
||||
whereConditions.push(inArray(sql`${commercials_json.data}->>'type'`, criteria.types));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(eq(schema.commercials.state, criteria.state));
|
||||
whereConditions.push(sql`(${commercials_json.data}->'location'->>'state') = ${criteria.state}`);
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
|
||||
whereConditions.push(gte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.minPrice));
|
||||
}
|
||||
|
||||
if (criteria.maxPrice) {
|
||||
whereConditions.push(lte(schema.commercials.price, criteria.maxPrice));
|
||||
whereConditions.push(lte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.maxPrice));
|
||||
}
|
||||
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
|
||||
whereConditions.push(sql`(${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`);
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional')));
|
||||
if (user?.role !== 'admin') {
|
||||
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;
|
||||
}
|
||||
// #### Find by criteria ########################################
|
||||
async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn.select({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
const query = this.conn.select({ commercial: commercials_json }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email));
|
||||
const whereConditions = this.getWhereConditions(criteria, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
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
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const data = await query;
|
||||
const results = data.map(r => r.commercial).map(r => convertDrizzleCommercialToCommercial(r));
|
||||
const totalCount = await this.getCommercialPropertiesCount(criteria);
|
||||
const results = data.map(r => ({ id: r.commercial.id, email: r.commercial.email, ...(r.commercial.data as CommercialPropertyListing) }));
|
||||
const totalCount = await this.getCommercialPropertiesCount(criteria, user);
|
||||
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(schema.commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria, user: JwtUser): 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 whereConditions = this.getWhereConditions(criteria, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
|
|
@ -91,77 +112,123 @@ export class CommercialPropertyService {
|
|||
|
||||
// #### Find by ID ########################################
|
||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
let result = await this.conn
|
||||
const conditions = [];
|
||||
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()
|
||||
.from(commercials)
|
||||
.where(and(sql`${commercials.id} = ${id}`));
|
||||
result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||
return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
|
||||
.from(commercials_json)
|
||||
.where(and(...conditions));
|
||||
if (result.length > 0) {
|
||||
return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing;
|
||||
} else {
|
||||
throw new BadRequestException(`No entry available for ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// #### Find by User EMail ########################################
|
||||
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
conditions.push(ne(commercials.draft, true));
|
||||
conditions.push(eq(commercials_json.email, email));
|
||||
if (email !== user?.email && user?.role !== 'admin') {
|
||||
conditions.push(sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`);
|
||||
}
|
||||
const listings = (await this.conn
|
||||
const listings = await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(and(...conditions))) as CommercialPropertyListing[];
|
||||
return listings.map(l => convertDrizzleCommercialToCommercial(l)) as CommercialPropertyListing[];
|
||||
.from(commercials_json)
|
||||
.where(and(...conditions));
|
||||
return listings.map(l => ({ id: l.id, email: l.email, ...(l.data as CommercialPropertyListing) }) 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 ########################################
|
||||
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
|
||||
const result = await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
|
||||
return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
|
||||
.from(commercials_json)
|
||||
.where(and(sql`(${commercials_json.data}->>'imagePath') = ${imagePath}`, sql`(${commercials_json.data}->>'serialId')::integer = ${serial}`));
|
||||
if (result.length > 0) {
|
||||
return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing;
|
||||
}
|
||||
}
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
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.updated = new Date();
|
||||
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
|
||||
const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
|
||||
const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning();
|
||||
return convertDrizzleCommercialToCommercial(createdListing);
|
||||
data.serialId = Number(serialId);
|
||||
CommercialPropertyListingSchema.parse(data);
|
||||
const { id, email, ...rest } = data;
|
||||
const convertedCommercialPropertyListing = { email, data: rest };
|
||||
const [createdListing] = await this.conn.insert(commercials_json).values(convertedCommercialPropertyListing).returning();
|
||||
return { id: createdListing.id, email: createdListing.email, ...(createdListing.data as CommercialPropertyListing) };
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
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;
|
||||
}
|
||||
}
|
||||
// #### UPDATE CommercialProps ########################################
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
try {
|
||||
const [existingListing] = await this.conn.select().from(commercials_json).where(eq(commercials_json.id, id));
|
||||
|
||||
if (!existingListing) {
|
||||
throw new NotFoundException(`Business listing with id ${id} not found`);
|
||||
}
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
|
||||
if (existingListing.email === user?.email || !user) {
|
||||
data.favoritesForUser = (<CommercialPropertyListing>existingListing.data).favoritesForUser || [];
|
||||
}
|
||||
CommercialPropertyListingSchema.parse(data);
|
||||
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
|
||||
let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
|
||||
const difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
|
||||
if (difference.length > 0) {
|
||||
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
|
||||
data.imageOrder = imageOrder;
|
||||
}
|
||||
const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
|
||||
const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning();
|
||||
return convertDrizzleCommercialToCommercial(updateListing);
|
||||
const { id: _, email, ...rest } = data;
|
||||
const convertedCommercialPropertyListing = { email, data: rest };
|
||||
const [updateListing] = await this.conn.update(commercials_json).set(convertedCommercialPropertyListing).where(eq(commercials_json.id, id)).returning();
|
||||
return { id: updateListing.id, email: updateListing.email, ...(updateListing.data as CommercialPropertyListing) };
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
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;
|
||||
}
|
||||
|
|
@ -170,30 +237,29 @@ export class CommercialPropertyService {
|
|||
// Images for commercial Properties
|
||||
// ##############################################################
|
||||
async deleteImage(imagePath: string, serial: string, name: string) {
|
||||
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||
const listing = await this.findByImagePath(imagePath, serial);
|
||||
const index = listing.imageOrder.findIndex(im => im === name);
|
||||
if (index > -1) {
|
||||
listing.imageOrder.splice(index, 1);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing, null);
|
||||
}
|
||||
}
|
||||
async addImage(imagePath: string, serial: string, imagename: string) {
|
||||
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||
const listing = await this.findByImagePath(imagePath, serial);
|
||||
listing.imageOrder.push(imagename);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing, null);
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
await this.conn.delete(commercials).where(eq(commercials.id, id));
|
||||
await this.conn.delete(commercials_json).where(eq(commercials_json.id, id));
|
||||
}
|
||||
// ##############################################################
|
||||
// States
|
||||
// ##############################################################
|
||||
async getStates(): Promise<any[]> {
|
||||
return await this.conn
|
||||
.select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
|
||||
.from(commercials)
|
||||
.groupBy(sql`${commercials.state}`)
|
||||
.orderBy(sql`count desc`);
|
||||
// #### DELETE Favorite ###################################
|
||||
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
|
||||
await this.conn
|
||||
.update(commercials_json)
|
||||
.set({
|
||||
data: sql`jsonb_set(${commercials_json.data}, '{favoritesForUser}', array_remove((${commercials_json.data}->>'favoritesForUser')::jsonb, ${user.email}))`,
|
||||
})
|
||||
.where(eq(commercials_json.id, id));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { BrokerListingsController } from './broker-listings.controller.js';
|
||||
import { BusinessListingsController } from './business-listings.controller.js';
|
||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { BrokerListingsController } from './broker-listings.controller';
|
||||
import { BusinessListingsController } from './business-listings.controller';
|
||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller';
|
||||
|
||||
import { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { BusinessListingService } from './business-listing.service.js';
|
||||
import { CommercialPropertyService } from './commercial-property.service.js';
|
||||
import { UnknownListingsController } from './unknown-listings.controller.js';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
import { UnknownListingsController } from './unknown-listings.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, AuthModule, GeoModule],
|
||||
imports: [DrizzleModule, AuthModule, GeoModule,FirebaseAdminModule],
|
||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||
exports: [BusinessListingService, CommercialPropertyService],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,25 @@
|
|||
import { Controller, Inject } from '@nestjs/common';
|
||||
import { Controller, Get, Inject, Param, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
|
||||
@Controller('listings/undefined')
|
||||
export class UnknownListingsController {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
private readonly businessListingsService: BusinessListingService,
|
||||
private readonly propertyListingsService: CommercialPropertyService,
|
||||
) {}
|
||||
|
||||
// @Get(':id')
|
||||
// async findById(@Param('id') id: string): Promise<any> {
|
||||
// const result = await this.listingsService.findById(id, businesses);
|
||||
// if (result) {
|
||||
// return result;
|
||||
// } else {
|
||||
// return await this.listingsService.findById(id, commercials);
|
||||
// }
|
||||
// }
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
try {
|
||||
return await this.businessListingsService.findBusinessesById(id, req.user);
|
||||
} catch (error) {
|
||||
return await this.propertyListingsService.findCommercialPropertiesById(id, req.user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { LogController } from './log.controller';
|
||||
|
||||
@Module({
|
||||
imports: [FirebaseAdminModule],
|
||||
controllers: [LogController],
|
||||
})
|
||||
export class LogModule {}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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>',
|
||||
},
|
||||
}));
|
||||
|
|
@ -1,16 +1,43 @@
|
|||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Post, UseGuards } 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 { MailService } from './mail.service.js';
|
||||
import { MailService } from './mail.service';
|
||||
|
||||
@Controller('mail')
|
||||
export class MailController {
|
||||
constructor(private mailService: MailService) {}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
async sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
if (mailInfo.listing) {
|
||||
return this.mailService.sendInquiry(mailInfo);
|
||||
return await this.mailService.sendInquiry(mailInfo);
|
||||
} else {
|
||||
return this.mailService.sendRequest(mailInfo);
|
||||
return await 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,49 @@
|
|||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||
import { Module } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { MailController } from './mail.controller.js';
|
||||
import { MailService } from './mail.service.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const user = process.env.amazon_user;
|
||||
const password = process.env.amazon_password;
|
||||
import { join } from 'path';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { MailController } from './mail.controller';
|
||||
import { MailService } from './mail.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DrizzleModule,
|
||||
UserModule,
|
||||
GeoModule,
|
||||
MailerModule.forRoot({
|
||||
transport: {
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: 'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
FirebaseAdminModule,
|
||||
MailerModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
transport: {
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.AMAZON_USER,
|
||||
pass: process.env.AMAZON_PASSWORD,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
template: {
|
||||
dir: join(__dirname, 'templates'),
|
||||
adapter: new HandlebarsAdapter(), // or new PugAdapter() or new EjsAdapter()
|
||||
options: {
|
||||
strict: true,
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
},
|
||||
template: {
|
||||
dir: join(__dirname, 'templates'),
|
||||
adapter: new HandlebarsAdapter({
|
||||
eq: function (a, b) {
|
||||
return a === b;
|
||||
},
|
||||
}),
|
||||
options: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [MailService, UserService, FileService, GeoService],
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getAuth } from 'firebase-admin/auth';
|
||||
import { join } from 'path';
|
||||
import { ZodError } from 'zod';
|
||||
import { SenderSchema } from '../models/db.model.js';
|
||||
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
import { SenderSchema, ShareByEMail, ShareByEMailSchema, User } from '../models/db.model';
|
||||
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model';
|
||||
import { UserService } from '../user/user.service';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
|
|
@ -18,7 +18,7 @@ export class MailService {
|
|||
|
||||
async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
SenderSchema.parse(mailInfo.sender);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
|
|
@ -53,9 +53,68 @@ export class MailService {
|
|||
},
|
||||
});
|
||||
}
|
||||
async sendVerificationEmail(
|
||||
email: string,
|
||||
redirectConfig: { protocol: string, hostname: string, port?: number }
|
||||
): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
// Firebase Auth-Instanz holen
|
||||
const auth = getAuth();
|
||||
|
||||
// Baue den Redirect-URL aus den übergebenen Parametern
|
||||
let continueUrl = `${redirectConfig.protocol}://${redirectConfig.hostname}`;
|
||||
if (redirectConfig.port) {
|
||||
continueUrl += `:${redirectConfig.port}`;
|
||||
}
|
||||
continueUrl += '/auth/verify-email-success'; // Beispiel für einen Weiterleitungspfad
|
||||
|
||||
// Custom Verification Link generieren
|
||||
const firebaseActionLink = await auth.generateEmailVerificationLink(email, {
|
||||
url: continueUrl,
|
||||
handleCodeInApp: false,
|
||||
});
|
||||
|
||||
// Extrahiere den oobCode aus dem Firebase Link
|
||||
const actionLinkUrl = new URL(firebaseActionLink);
|
||||
const oobCode = actionLinkUrl.searchParams.get('oobCode');
|
||||
|
||||
if (!oobCode) {
|
||||
throw new BadRequestException('Failed to generate verification code');
|
||||
}
|
||||
|
||||
// Erstelle die benutzerdefinierte URL mit dem oobCode
|
||||
let customActionLink = `${redirectConfig.protocol}://${redirectConfig.hostname}`;
|
||||
if (redirectConfig.port) {
|
||||
customActionLink += `:${redirectConfig.port}`;
|
||||
}
|
||||
|
||||
// Ersetze die Platzhalter mit den tatsächlichen Werten
|
||||
customActionLink += `/email-authorized?email=${encodeURIComponent(email)}&mode=verifyEmail&oobCode=${oobCode}`;
|
||||
|
||||
// Zufallszahl für die E-Mail generieren
|
||||
const randomNumber = Math.floor(Math.random() * 10000);
|
||||
|
||||
// E-Mail senden
|
||||
await this.mailerService.sendMail({
|
||||
to: email,
|
||||
from: '"Bizmatch Team" <info@bizmatch.net>',
|
||||
subject: 'Verify your email address',
|
||||
template: join(__dirname, '../..', 'mail/templates/email-verification.hbs'),
|
||||
context: {
|
||||
actionLink: customActionLink,
|
||||
randomNumber: randomNumber
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Error sending verification email:', error);
|
||||
throw new BadRequestException('Failed to send verification email');
|
||||
}
|
||||
}
|
||||
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
SenderSchema.parse(mailInfo.sender);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
|
|
@ -81,4 +140,48 @@ 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"> <!-- utf-8 works for most cases -->
|
||||
<meta name="viewport" content="width=device-width"> <!-- Forcing initial-scale shouldn't be necessary -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <!-- Use the latest (edge) version of IE rendering engine -->
|
||||
<meta name="x-apple-disable-message-reformatting"> <!-- Disable auto-scale in iOS 10 Mail entirely -->
|
||||
<title>Email address verification</title> <!-- The title tag shows in email notifications, like Android 4.4. -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,700" rel="stylesheet">
|
||||
|
||||
<!-- CSS Reset : BEGIN -->
|
||||
<style>
|
||||
/* What it does: Remove spaces around the email design added by some email clients. */
|
||||
/* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
|
||||
html,
|
||||
body {
|
||||
margin: 0 auto !important;
|
||||
padding: 0 !important;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
background: #f1f1f1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* What it does: Stops email clients resizing small text. */
|
||||
* {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* What it does: Centers email on Android 4.4 */
|
||||
div[style*="margin: 16px 0"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* What it does: Stops Outlook from adding extra spacing to tables. */
|
||||
table,
|
||||
td {
|
||||
mso-table-lspace: 0pt !important;
|
||||
mso-table-rspace: 0pt !important;
|
||||
}
|
||||
|
||||
/* What it does: Fixes webkit padding issue. */
|
||||
table {
|
||||
border-spacing: 0 !important;
|
||||
border-collapse: collapse !important;
|
||||
table-layout: fixed !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
/* What it does: Uses a better rendering method when resizing images in IE. */
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
/* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* What it does: A work-around for email clients meddling in triggered links. */
|
||||
*[x-apple-data-detectors],
|
||||
/* iOS */
|
||||
.unstyle-auto-detected-links *,
|
||||
.aBn {
|
||||
border-bottom: 0 !important;
|
||||
cursor: default !important;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
/* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
|
||||
.a6S {
|
||||
display: none !important;
|
||||
opacity: 0.01 !important;
|
||||
}
|
||||
|
||||
/* What it does: Prevents Gmail from changing the text color in conversation threads. */
|
||||
.im {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* If the above doesn't work, add a .g-img class to any image in question. */
|
||||
img.g-img+div {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
|
||||
/* Create one of these media queries for each additional viewport size you'd like to fix */
|
||||
|
||||
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
|
||||
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
|
||||
u~div .email-container {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 6, 6S, 7, 8, and X */
|
||||
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
|
||||
u~div .email-container {
|
||||
min-width: 375px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPhone 6+, 7+, and 8+ */
|
||||
@media only screen and (min-device-width: 414px) {
|
||||
u~div .email-container {
|
||||
min-width: 414px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- CSS Reset : END -->
|
||||
|
||||
<!-- Progressive Enhancements : BEGIN -->
|
||||
<style>
|
||||
.primary {
|
||||
background: #30e3ca;
|
||||
}
|
||||
|
||||
.bg_white {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.bg_light {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.bg_black {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.bg_dark {
|
||||
background: rgba(0, 0, 0, .8);
|
||||
}
|
||||
|
||||
.email-section {
|
||||
padding: 2.5em;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
/*HERO*/
|
||||
.hero {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero .text {
|
||||
color: rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
.hero .text h2 {
|
||||
color: #000;
|
||||
font-size: 40px;
|
||||
margin-bottom: 0;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hero .text h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.hero .text h2 span {
|
||||
font-weight: 600;
|
||||
color: #30e3ca;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
display: block;
|
||||
color: black;
|
||||
line-height: 32px;
|
||||
font-weight: 300;
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, sans-serif;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@media (max-width:400px) {
|
||||
.hero img {
|
||||
width: 200px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body width="100%"
|
||||
style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f1f1f1; display: flex; align-items: center; justify-content: center;">
|
||||
<div style="width: 100%; background-color: #f1f1f1;">
|
||||
<div
|
||||
style="display: none; font-size: 1px;max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
|
||||
Hello, click on the button below to verify your email address
|
||||
</div>
|
||||
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
|
||||
<!-- BEGIN BODY -->
|
||||
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
|
||||
style="margin: auto;">
|
||||
<tr>
|
||||
<td valign="middle" class="hero bg_white" style="padding: 3em 0 2em 0;">
|
||||
<img src="https://github.com/ColorlibHQ/email-templates/blob/master/10/images/email.png?raw=true"
|
||||
alt="" class="g-img" style="width: 200px; height: auto; margin: auto; display: block;">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- end tr -->
|
||||
<tr>
|
||||
<td valign="middle" class="hero bg_white" style="padding: 2em 0 4em 0;">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="text" style="padding: 0 2.5em; text-align: center;">
|
||||
<h2 style="margin-bottom: 20px; font-size: 32px;">Verify your email address</h2>
|
||||
<p class="email-body">
|
||||
Thanks for signup with us. Click on the button below to verify your email
|
||||
address.
|
||||
</p>
|
||||
<a href="{{actionLink}}" target="_blank"
|
||||
style="padding:15px 40px; background-color: #5D91E8; color: white;">Verify
|
||||
your email</a>
|
||||
<p class="email-body">
|
||||
If this email wasn't intended for you feel free to delete it.<br />
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- end tr -->
|
||||
<span style="color: #f1f1f1; display: none;">{{randomNumber}}</span>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<!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>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<!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>
|
||||
|
|
@ -1,18 +1,24 @@
|
|||
import { LoggerService } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import express from 'express';
|
||||
import { AppModule } from './app.module.js';
|
||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const server = express();
|
||||
server.set('trust proxy', true);
|
||||
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.enableCors({
|
||||
origin: '*',
|
||||
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization, x-hide-loading',
|
||||
});
|
||||
//origin: 'http://localhost:4200',
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
|
|
|||
|
|
@ -17,20 +17,25 @@ export interface UserData {
|
|||
hasCompanyLogo?: boolean;
|
||||
licensedIn?: string[];
|
||||
gender?: 'male' | 'female';
|
||||
customerType?: 'buyer' | 'broker' | 'professional';
|
||||
customerType?: 'buyer' | 'seller' | 'professional';
|
||||
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
created?: 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 CustomerType = 'buyer' | 'professional';
|
||||
export type CustomerType = 'buyer' | 'seller' | 'professional';
|
||||
export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
export type ListingsCategory = 'commercialProperty' | 'business';
|
||||
|
||||
export const GenderEnum = z.enum(['male', 'female']);
|
||||
export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
|
||||
export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']);
|
||||
export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']);
|
||||
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
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 TypeEnum = z.enum([
|
||||
'automotive',
|
||||
|
|
@ -102,50 +107,73 @@ const USStates = z.enum([
|
|||
'WY',
|
||||
]);
|
||||
export const AreasServedSchema = z.object({
|
||||
county: z.string().nonempty('County is required'),
|
||||
state: z.string().nonempty('State is required'),
|
||||
county: z.string().optional().nullable(),
|
||||
state: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine(val => val !== null && val !== '', {
|
||||
message: 'State is required',
|
||||
}),
|
||||
});
|
||||
|
||||
export const LicensedInSchema = z.object({
|
||||
registerNo: z.string().nonempty('Registration number is required'),
|
||||
state: z.string().nonempty('State is required'),
|
||||
state: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine(val => val !== null && val !== '', {
|
||||
message: 'State is required',
|
||||
}),
|
||||
registerNo: z.string().nonempty('License number is required'),
|
||||
});
|
||||
export const GeoSchema = z.object({
|
||||
city: z.string(),
|
||||
state: z.string().refine(val => USStates.safeParse(val).success, {
|
||||
message: 'Invalid state. Must be a valid 2-letter US state code.',
|
||||
}),
|
||||
latitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -90 && value <= 90;
|
||||
},
|
||||
{
|
||||
message: 'Latitude muss zwischen -90 und 90 liegen',
|
||||
},
|
||||
),
|
||||
longitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -180 && value <= 180;
|
||||
},
|
||||
{
|
||||
message: 'Longitude muss zwischen -180 und 180 liegen',
|
||||
},
|
||||
),
|
||||
});
|
||||
const phoneRegex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
|
||||
|
||||
export const GeoSchema = z
|
||||
.object({
|
||||
name: z.string().optional().nullable(),
|
||||
state: z.string().refine(val => USStates.safeParse(val).success, {
|
||||
message: 'Invalid state. Must be a valid 2-letter US state code.',
|
||||
}),
|
||||
latitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -90 && value <= 90;
|
||||
},
|
||||
{
|
||||
message: 'Latitude muss zwischen -90 und 90 liegen',
|
||||
},
|
||||
),
|
||||
longitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -180 && value <= 180;
|
||||
},
|
||||
{
|
||||
message: 'Longitude muss zwischen -180 und 180 liegen',
|
||||
},
|
||||
),
|
||||
county: z.string().optional().nullable(),
|
||||
housenumber: z.string().optional().nullable(),
|
||||
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
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
firstname: z.string().min(2, { message: 'First name must contain at least 2 characters' }),
|
||||
lastname: z.string().min(2, { message: 'Last name must contain at least 2 characters' }),
|
||||
firstname: z.string().min(3, { message: 'First name must contain at least 2 characters' }),
|
||||
lastname: z.string().min(3, { message: 'Last name must contain at least 2 characters' }),
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
phoneNumber: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
companyName: z.string().optional().nullable(),
|
||||
companyOverview: z.string().optional().nullable(),
|
||||
companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
|
||||
companyLocation: GeoSchema.optional().nullable(),
|
||||
location: GeoSchema.optional().nullable(),
|
||||
offeredServices: z.string().optional().nullable(),
|
||||
areasServed: z.array(AreasServedSchema).optional().nullable(),
|
||||
hasProfile: z.boolean().optional().nullable(),
|
||||
|
|
@ -156,6 +184,9 @@ export const UserSchema = z
|
|||
customerSubType: CustomerSubTypeEnum.optional().nullable(),
|
||||
created: z.date().optional().nullable(),
|
||||
updated: z.date().optional().nullable(),
|
||||
subscriptionId: z.string().optional().nullable(),
|
||||
subscriptionPlan: SubscriptionTypeEnum.optional().nullable(),
|
||||
showInDirectory: z.boolean(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.customerType === 'professional') {
|
||||
|
|
@ -166,7 +197,13 @@ export const UserSchema = z
|
|||
path: ['customerSubType'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.companyName || data.companyName.length < 6) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company Name must contain at least 6 characters for professional customers',
|
||||
path: ['companyName'],
|
||||
});
|
||||
}
|
||||
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
|
|
@ -199,11 +236,11 @@ export const UserSchema = z
|
|||
});
|
||||
}
|
||||
|
||||
if (!data.companyLocation) {
|
||||
if (!data.location) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company location is required for professional customers',
|
||||
path: ['companyLocation'],
|
||||
path: ['location'],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -221,35 +258,61 @@ export type AreasServed = z.infer<typeof AreasServedSchema>;
|
|||
export type LicensedIn = z.infer<typeof LicensedInSchema>;
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
export const BusinessListingSchema = z.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
email: z.string().email(),
|
||||
type: z.string().refine(val => TypeEnum.safeParse(val).success, {
|
||||
message: 'Invalid type. Must be one of: ' + TypeEnum.options.join(', '),
|
||||
}),
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
draft: z.boolean(),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
realEstateIncluded: z.boolean().optional().nullable(),
|
||||
leasedLocation: z.boolean().optional().nullable(),
|
||||
franchiseResale: z.boolean().optional().nullable(),
|
||||
salesRevenue: z.number().positive().max(100000000),
|
||||
cashFlow: z.number().positive().max(100000000),
|
||||
supportAndTraining: z.string().min(5),
|
||||
employees: z.number().int().positive().max(100000).optional().nullable(),
|
||||
established: z.number().int().min(1800).max(2030).optional().nullable(),
|
||||
internalListingNumber: z.number().int().positive().optional().nullable(),
|
||||
reasonForSale: z.string().min(5).optional().nullable(),
|
||||
brokerLicencing: z.string().min(5).optional().nullable(),
|
||||
internals: z.string().min(5).optional().nullable(),
|
||||
imageName: z.string().optional().nullable(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
});
|
||||
export const BusinessListingSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
email: z.string().email(),
|
||||
type: z.string().refine(val => TypeEnum.safeParse(val).success, {
|
||||
message: 'Invalid type. Must be one of: ' + TypeEnum.options.join(', '),
|
||||
}),
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().optional().nullable(),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
draft: z.boolean(),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
realEstateIncluded: z.boolean().optional().nullable(),
|
||||
leasedLocation: z.boolean().optional().nullable(),
|
||||
franchiseResale: z.boolean().optional().nullable(),
|
||||
salesRevenue: z.number().positive().nullable(),
|
||||
cashFlow: z.number().optional().nullable(),
|
||||
ffe: z.number().optional().nullable(),
|
||||
inventory: z.number().optional().nullable(),
|
||||
supportAndTraining: z.string().min(5).optional().nullable(),
|
||||
employees: z.number().int().positive().max(100000).optional().nullable(),
|
||||
established: z.number().int().min(1).max(250).optional().nullable(),
|
||||
internalListingNumber: z.number().int().positive().optional().nullable(),
|
||||
reasonForSale: z.string().min(5).optional().nullable(),
|
||||
brokerLicencing: z.string().optional().nullable(),
|
||||
internals: z.string().min(5).optional().nullable(),
|
||||
imageName: z.string().optional().nullable(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.price && data.price > 1000000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Price must less than or equal $1,000,000,000',
|
||||
path: ['price'],
|
||||
});
|
||||
}
|
||||
if (data.salesRevenue && data.salesRevenue > 100000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'SalesRevenue must less than or equal $100,000,000',
|
||||
path: ['salesRevenue'],
|
||||
});
|
||||
}
|
||||
if (data.cashFlow && data.cashFlow > 100000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'CashFlow must less than or equal $100,000,000',
|
||||
path: ['cashFlow'],
|
||||
});
|
||||
}
|
||||
});
|
||||
export type BusinessListing = z.infer<typeof BusinessListingSchema>;
|
||||
|
||||
export const CommercialPropertyListingSchema = z
|
||||
|
|
@ -263,16 +326,25 @@ export const CommercialPropertyListingSchema = z
|
|||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
price: z.number().positive().optional().nullable(),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
internalListingNumber: z.number().int().positive().optional().nullable(),
|
||||
draft: z.boolean(),
|
||||
imageOrder: z.array(z.string()),
|
||||
imagePath: z.string().nullable().optional(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
})
|
||||
.strict();
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.price && data.price > 1000000000) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Price must less than or equal $1,000,000,000',
|
||||
path: ['price'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type CommercialPropertyListing = z.infer<typeof CommercialPropertyListingSchema>;
|
||||
|
||||
|
|
@ -288,3 +360,30 @@ export const SenderSchema = z.object({
|
|||
comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }),
|
||||
});
|
||||
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>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
||||
import Stripe from 'stripe';
|
||||
import { BusinessListing, CommercialPropertyListing, Sender, SortByOptions, SortByTypes, User } from './db.model';
|
||||
import { State } from './server.model';
|
||||
|
||||
export interface StatesResult {
|
||||
state: string;
|
||||
|
|
@ -9,6 +11,12 @@ export interface KeyValue {
|
|||
name: string;
|
||||
value: string;
|
||||
}
|
||||
export interface KeyValueAsSortBy {
|
||||
name: string;
|
||||
value: SortByOptions;
|
||||
type?: SortByTypes;
|
||||
selectName?: string;
|
||||
}
|
||||
export interface KeyValueRatio {
|
||||
label: string;
|
||||
value: number;
|
||||
|
|
@ -59,12 +67,13 @@ export interface ListCriteria {
|
|||
page: number;
|
||||
types: string[];
|
||||
state: string;
|
||||
city: string;
|
||||
city: GeoResult;
|
||||
prompt: string;
|
||||
searchType: 'exact' | 'radius';
|
||||
// radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500';
|
||||
radius: number;
|
||||
criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
|
||||
sortBy?: SortByOptions;
|
||||
}
|
||||
export interface BusinessListingCriteria extends ListCriteria {
|
||||
minPrice: number;
|
||||
|
|
@ -75,13 +84,13 @@ export interface BusinessListingCriteria extends ListCriteria {
|
|||
maxCashFlow: number;
|
||||
minNumberEmployees: number;
|
||||
maxNumberEmployees: number;
|
||||
establishedSince: number;
|
||||
establishedUntil: number;
|
||||
establishedMin: number;
|
||||
realEstateChecked: boolean;
|
||||
leasedLocation: boolean;
|
||||
franchiseResale: boolean;
|
||||
title: string;
|
||||
brokerName: string;
|
||||
email: string;
|
||||
criteriaType: 'businessListings';
|
||||
}
|
||||
export interface CommercialPropertyListingCriteria extends ListCriteria {
|
||||
|
|
@ -91,8 +100,7 @@ export interface CommercialPropertyListingCriteria extends ListCriteria {
|
|||
criteriaType: 'commercialPropertyListings';
|
||||
}
|
||||
export interface UserListingCriteria extends ListCriteria {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
brokerName: string;
|
||||
companyName: string;
|
||||
counties: string[];
|
||||
criteriaType: 'brokerListings';
|
||||
|
|
@ -112,13 +120,16 @@ export interface KeycloakUser {
|
|||
requiredActions?: any[];
|
||||
notBefore?: number;
|
||||
access?: Access;
|
||||
attributes?: Attributes;
|
||||
}
|
||||
export interface JwtUser {
|
||||
userId: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
roles: string[];
|
||||
email: string;
|
||||
role: string;
|
||||
uid: string;
|
||||
}
|
||||
interface Attributes {
|
||||
[key: string]: any;
|
||||
priceID: any;
|
||||
}
|
||||
export interface Access {
|
||||
manageGroupMembership: boolean;
|
||||
|
|
@ -166,6 +177,7 @@ export interface JwtToken {
|
|||
family_name: string;
|
||||
email: string;
|
||||
user_id: string;
|
||||
price_id: string;
|
||||
}
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
|
|
@ -224,24 +236,66 @@ export interface UploadParams {
|
|||
}
|
||||
export interface GeoResult {
|
||||
id: number;
|
||||
city: string;
|
||||
name: string;
|
||||
street?: string;
|
||||
housenumber?: string;
|
||||
county?: string;
|
||||
zipCode?: number;
|
||||
state: string;
|
||||
// state_code: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
export interface CityAndStateResult {
|
||||
interface CityResult {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
state: string;
|
||||
type: 'city';
|
||||
content: GeoResult;
|
||||
}
|
||||
|
||||
interface StateResult {
|
||||
id: number;
|
||||
type: 'state';
|
||||
content: State;
|
||||
}
|
||||
export type CityAndStateResult = CityResult | StateResult;
|
||||
export interface CountyResult {
|
||||
id: number;
|
||||
name: string;
|
||||
state: 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 {
|
||||
// Check for undefined or null
|
||||
if (value === undefined || value === null) {
|
||||
|
|
@ -281,7 +335,7 @@ export interface ValidationMessage {
|
|||
field: string;
|
||||
message: string;
|
||||
}
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string, subscriptionPlan: 'professional' | 'broker'): User {
|
||||
return {
|
||||
id: undefined,
|
||||
email,
|
||||
|
|
@ -292,7 +346,7 @@ export function createDefaultUser(email: string, firstname: string, lastname: st
|
|||
companyName: null,
|
||||
companyOverview: null,
|
||||
companyWebsite: null,
|
||||
companyLocation: null,
|
||||
location: null,
|
||||
offeredServices: null,
|
||||
areasServed: [],
|
||||
hasProfile: false,
|
||||
|
|
@ -303,6 +357,8 @@ export function createDefaultUser(email: string, firstname: string, lastname: st
|
|||
customerSubType: null,
|
||||
created: new Date(),
|
||||
updated: new Date(),
|
||||
subscriptionId: null,
|
||||
subscriptionPlan: subscriptionPlan,
|
||||
};
|
||||
}
|
||||
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
|
||||
|
|
@ -352,3 +408,25 @@ export function createDefaultBusinessListing(): BusinessListing {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,3 +70,34 @@ export interface CountyRequest {
|
|||
prefix: 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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { getRealIpInfo } from 'src/utils/ip.util';
|
||||
|
||||
@Injectable()
|
||||
export class RequestDurationMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(RequestDurationMiddleware.name);
|
||||
|
||||
constructor(private readonly cls: ClsService) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
// const duration = Date.now() - start;
|
||||
// this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
||||
const duration = Date.now() - start;
|
||||
let logMessage = `${req.method} ${req.url} - ${duration}ms`;
|
||||
const { ip, countryCode } = getRealIpInfo(req);
|
||||
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
const body = JSON.stringify(req.body);
|
||||
logMessage += ` - Body: ${body}`;
|
||||
}
|
||||
// Setze die IP-Adresse und den Ländercode im CLS-Kontext
|
||||
try {
|
||||
this.cls.set('ip', ip);
|
||||
this.cls.set('countryCode', countryCode);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to set CLS context', error);
|
||||
}
|
||||
|
||||
// const start = Date.now();
|
||||
|
||||
// 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);
|
||||
// });
|
||||
|
||||
this.logger.log(logMessage);
|
||||
});
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { SelectOptionsService } from './select-options.service.js';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { SelectOptionsService } from './select-options.service';
|
||||
|
||||
@Controller('select-options')
|
||||
export class SelectOptionsController {
|
||||
constructor(private selectOptionsService: SelectOptionsService) {}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get()
|
||||
getSelectOption(): any {
|
||||
return {
|
||||
|
|
@ -15,6 +18,7 @@ export class SelectOptionsController {
|
|||
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
|
||||
customerSubTypes: this.selectOptionsService.customerSubTypes,
|
||||
distances: this.selectOptionsService.distances,
|
||||
sortByOptions: this.selectOptionsService.sortByOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SelectOptionsController } from './select-options.controller.js';
|
||||
import { SelectOptionsService } from './select-options.service.js';
|
||||
import { FirebaseAdminModule } from '../firebase-admin/firebase-admin.module';
|
||||
import { SelectOptionsController } from './select-options.controller';
|
||||
import { SelectOptionsService } from './select-options.service';
|
||||
|
||||
@Module({
|
||||
controllers: [SelectOptionsController],
|
||||
providers: [SelectOptionsService]
|
||||
})
|
||||
imports: [FirebaseAdminModule],
|
||||
controllers: [SelectOptionsController],
|
||||
providers: [SelectOptionsService],
|
||||
})
|
||||
export class SelectOptionsModule {}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js';
|
||||
import { ImageType, KeyValue, KeyValueAsSortBy, KeyValueStyle } from '../models/main.model';
|
||||
|
||||
@Injectable()
|
||||
export class SelectOptionsService {
|
||||
constructor() {}
|
||||
public typesOfBusiness: Array<KeyValueStyle> = [
|
||||
{ name: 'Automotive', value: 'automotive', oldValue: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||
{ name: 'Automotive', value: 'automotive', oldValue: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-500' },
|
||||
{ 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: 'Real Estate', value: 'realEstate', oldValue: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
|
||||
|
|
@ -35,7 +35,19 @@ export class SelectOptionsService {
|
|||
{ name: '$1M', value: '1000000' },
|
||||
{ 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> = [
|
||||
{ name: '5 miles', value: '5' },
|
||||
{ name: '20 miles', value: '20' },
|
||||
|
|
@ -52,6 +64,7 @@ export class SelectOptionsService {
|
|||
];
|
||||
public customerTypes: Array<KeyValue> = [
|
||||
{ name: 'Buyer', value: 'buyer' },
|
||||
{ name: 'Commercial Property Seller', value: 'seller' },
|
||||
{ name: 'Professional', value: 'professional' },
|
||||
];
|
||||
public customerSubTypes: Array<KeyValue> = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({ name: 'setup-admin', description: 'Set up the first admin user' })
|
||||
export class SetupAdminCommand extends CommandRunner {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(passedParams: string[]): Promise<void> {
|
||||
if (passedParams.length < 1) {
|
||||
console.error('Please provide a user UID');
|
||||
return;
|
||||
}
|
||||
|
||||
const uid = passedParams[0];
|
||||
|
||||
try {
|
||||
await this.authService.setUserRole(uid, 'admin');
|
||||
console.log(`User ${uid} has been set as admin`);
|
||||
} catch (error) {
|
||||
console.error('Error setting admin role:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { ZodError } from 'zod';
|
||||
import { FileService } from '../file/file.service';
|
||||
|
||||
import { AdminGuard } from 'src/jwt-auth/admin-auth.guard';
|
||||
import { AuthGuard } from 'src/jwt-auth/auth.guard';
|
||||
import { OptionalAuthGuard } from 'src/jwt-auth/optional-auth.guard';
|
||||
import { User } from '../models/db.model';
|
||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model.js';
|
||||
import { UserService } from './user.service.js';
|
||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
|
|
@ -14,52 +18,70 @@ export class UserController {
|
|||
private fileService: FileService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get()
|
||||
findByMail(@Request() req, @Query('mail') mail: string): any {
|
||||
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)}`);
|
||||
async findByMail(@Request() req, @Query('mail') mail: string): Promise<User> {
|
||||
const user = await this.userService.getUserByMail(mail, req.user as JwtUser);
|
||||
return user;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Param('id') id: string): any {
|
||||
this.logger.info(`Searching for user with ID: ${id}`);
|
||||
const user = this.userService.getUserById(id);
|
||||
this.logger.info(`Found user: ${JSON.stringify(user)}`);
|
||||
async findById(@Param('id') id: string): Promise<User> {
|
||||
const user = await this.userService.getUserById(id);
|
||||
return user;
|
||||
}
|
||||
@UseGuards(AdminGuard)
|
||||
@Get('user/all')
|
||||
async getAllUser(): Promise<User[]> {
|
||||
return await this.userService.getAllUser();
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post()
|
||||
save(@Body() user: any): Promise<User> {
|
||||
this.logger.info(`Saving user: ${JSON.stringify(user)}`);
|
||||
const savedUser = this.userService.saveUser(user);
|
||||
this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);
|
||||
async save(@Body() user: any): Promise<User> {
|
||||
try {
|
||||
const savedUser = await this.userService.saveUser(user);
|
||||
return 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;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('search')
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
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)}`);
|
||||
async find(@Body() criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
|
||||
const foundUsers = await this.userService.searchUserListings(criteria);
|
||||
return foundUsers;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalAuthGuard)
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: UserListingCriteria): Promise<number> {
|
||||
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;
|
||||
async findTotal(@Body() criteria: UserListingCriteria): Promise<number> {
|
||||
return await this.userService.getUserListingsCount(criteria);
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('subscriptions/:id')
|
||||
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
|
||||
const subscriptions = this.fileService.getSubscriptions();
|
||||
const subscriptions = [];
|
||||
const user = await this.userService.getUserById(id);
|
||||
subscriptions.forEach(s => {
|
||||
s.userId = user.id;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { UserController } from './user.controller.js';
|
||||
import { UserService } from './user.service.js';
|
||||
import { FirebaseAdminModule } from 'src/firebase-admin/firebase-admin.module';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, GeoModule],
|
||||
imports: [DrizzleModule, GeoModule,FirebaseAdminModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService, FileService, GeoService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, asc, count, desc, eq, inArray, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { User, UserSchema } from '../models/db.model.js';
|
||||
import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
|
||||
import { convertDrizzleUserToUser, convertUserToDrizzleUser, getDistanceQuery } from '../utils.js';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { User, UserSchema } from '../models/db.model';
|
||||
import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model';
|
||||
import { getDistanceQuery, splitName } from '../utils';
|
||||
|
||||
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
|
||||
@Injectable()
|
||||
|
|
@ -24,56 +23,68 @@ export class UserService {
|
|||
|
||||
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
whereConditions.push(eq(schema.users.customerType, 'professional'));
|
||||
whereConditions.push(sql`(${schema.users_json.data}->>'customerType') = 'professional'`);
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(schema.users.city, `%${criteria.city}%`));
|
||||
whereConditions.push(sql`(${schema.users_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`);
|
||||
}
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
||||
whereConditions.push(sql`${getDistanceQuery(schema.users_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
|
||||
whereConditions.push(inArray(schema.users.customerSubType, criteria.types as CustomerSubType[]));
|
||||
whereConditions.push(inArray(sql`${schema.users_json.data}->>'customerSubType'`, criteria.types as CustomerSubType[]));
|
||||
}
|
||||
|
||||
if (criteria.firstname) {
|
||||
whereConditions.push(ilike(schema.users.firstname, `%${criteria.firstname}%`));
|
||||
}
|
||||
|
||||
if (criteria.lastname) {
|
||||
whereConditions.push(ilike(schema.users.lastname, `%${criteria.lastname}%`));
|
||||
if (criteria.brokerName) {
|
||||
const { firstname, lastname } = splitName(criteria.brokerName);
|
||||
whereConditions.push(sql`(${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`);
|
||||
}
|
||||
|
||||
if (criteria.companyName) {
|
||||
whereConditions.push(ilike(schema.users.companyName, `%${criteria.companyName}%`));
|
||||
whereConditions.push(sql`(${schema.users_json.data}->>'companyName') ILIKE ${`%${criteria.companyName}%`}`);
|
||||
}
|
||||
|
||||
if (criteria.counties && criteria.counties.length > 0) {
|
||||
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}%`})`)));
|
||||
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}%`})`)));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`);
|
||||
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users_json.data}->'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;
|
||||
}
|
||||
async searchUserListings(criteria: UserListingCriteria) {
|
||||
async searchUserListings(criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn.select().from(schema.users);
|
||||
const query = this.conn.select().from(schema.users_json);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
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
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const data = await query;
|
||||
const results = data.map(r => convertDrizzleUserToUser(r));
|
||||
const results = data.map(u => ({ id: u.id, email: u.email, ...(u.data as User) }) as User);
|
||||
const totalCount = await this.getUserListingsCount(criteria);
|
||||
|
||||
return {
|
||||
|
|
@ -82,7 +93,7 @@ export class UserService {
|
|||
};
|
||||
}
|
||||
async getUserListingsCount(criteria: UserListingCriteria): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(schema.users);
|
||||
const countQuery = this.conn.select({ value: count() }).from(schema.users_json);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
|
|
@ -94,33 +105,31 @@ export class UserService {
|
|||
return totalCount;
|
||||
}
|
||||
async getUserByMail(email: string, jwtuser?: JwtUser) {
|
||||
const users = (await this.conn
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(sql`email = ${email}`)) as User[];
|
||||
const users = await this.conn.select().from(schema.users_json).where(eq(schema.users_json.email, email));
|
||||
if (users.length === 0) {
|
||||
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname) };
|
||||
const u = await this.saveUser(user);
|
||||
return convertDrizzleUserToUser(u);
|
||||
const user: User = { id: undefined, customerType: 'professional', ...createDefaultUser(email, '', '', null) };
|
||||
const u = await this.saveUser(user, false);
|
||||
return u;
|
||||
} else {
|
||||
const user = users[0];
|
||||
const user = { id: users[0].id, email: users[0].email, ...(users[0].data as User) } as User;
|
||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||
return convertDrizzleUserToUser(user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
async getUserById(id: string) {
|
||||
const users = (await this.conn
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(sql`id = ${id}`)) as User[];
|
||||
const users = await this.conn.select().from(schema.users_json).where(eq(schema.users_json.id, id));
|
||||
|
||||
const user = users[0];
|
||||
const user = { id: users[0].id, email: users[0].email, ...(users[0].data as User) } as User;
|
||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||
return convertDrizzleUserToUser(user);
|
||||
return user;
|
||||
}
|
||||
async saveUser(user: User): Promise<User> {
|
||||
async getAllUser() {
|
||||
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 {
|
||||
user.updated = new Date();
|
||||
if (user.id) {
|
||||
|
|
@ -128,29 +137,22 @@ export class UserService {
|
|||
} else {
|
||||
user.created = new Date();
|
||||
}
|
||||
const validatedUser = UserSchema.parse(user);
|
||||
const drizzleUser = convertUserToDrizzleUser(validatedUser);
|
||||
let validatedUser = user;
|
||||
if (processValidation) {
|
||||
validatedUser = UserSchema.parse(user);
|
||||
}
|
||||
//const drizzleUser = convertUserToDrizzleUser(validatedUser);
|
||||
const { id: _, ...rest } = validatedUser;
|
||||
const drizzleUser = { email: user.email, data: rest };
|
||||
if (user.id) {
|
||||
const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning();
|
||||
return convertDrizzleUserToUser(updateUser) as User;
|
||||
const [updateUser] = await this.conn.update(schema.users_json).set(drizzleUser).where(eq(schema.users_json.id, user.id)).returning();
|
||||
return { id: updateUser.id, email: updateUser.email, ...(updateUser.data as User) } as User;
|
||||
} else {
|
||||
const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning();
|
||||
return convertDrizzleUserToUser(newUser) as User;
|
||||
const [newUser] = await this.conn.insert(schema.users_json).values(drizzleUser).returning();
|
||||
return { id: newUser.id, email: newUser.email, ...(newUser.data as User) } as User;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { sql } from 'drizzle-orm';
|
||||
import { businesses, commercials, users } from './drizzle/schema.js';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from './models/db.model.js';
|
||||
import { businesses, businesses_json, commercials, commercials_json, users, users_json } from './drizzle/schema';
|
||||
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
|
||||
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
|
||||
export function convertStringToNullUndefined(value) {
|
||||
|
|
@ -17,109 +16,31 @@ export function convertStringToNullUndefined(value) {
|
|||
return value;
|
||||
}
|
||||
|
||||
export const getDistanceQuery = (schema: typeof businesses | typeof commercials | typeof users, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => {
|
||||
export const getDistanceQuery = (schema: typeof businesses_json | typeof commercials_json | typeof users_json, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => {
|
||||
const radius = unit === 'km' ? EARTH_RADIUS_KM : EARTH_RADIUS_MILES;
|
||||
|
||||
return sql`
|
||||
${radius} * 2 * ASIN(SQRT(
|
||||
POWER(SIN((${lat} - ${schema.latitude}) * PI() / 180 / 2), 2) +
|
||||
COS(${lat} * PI() / 180) * COS(${schema.latitude} * PI() / 180) *
|
||||
POWER(SIN((${lon} - ${schema.longitude}) * PI() / 180 / 2), 2)
|
||||
POWER(SIN((${lat} - (${schema.data}->'location'->>'latitude')::float) * PI() / 180 / 2), 2) +
|
||||
COS(${lat} * PI() / 180) * COS((${schema.data}->'location'->>'latitude')::float * PI() / 180) *
|
||||
POWER(SIN((${lon} - (${schema.data}->'location'->>'longitude')::float) * PI() / 180 / 2), 2)
|
||||
))
|
||||
`;
|
||||
};
|
||||
|
||||
type DrizzleUser = typeof users.$inferSelect;
|
||||
type DrizzleBusinessListing = typeof businesses.$inferSelect;
|
||||
type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
|
||||
export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing {
|
||||
return flattenObject(businessListing);
|
||||
}
|
||||
export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing {
|
||||
const o = {
|
||||
location_city: drizzleBusinessListing.city,
|
||||
location_state: drizzleBusinessListing.state,
|
||||
location_latitude: drizzleBusinessListing.latitude,
|
||||
location_longitude: drizzleBusinessListing.longitude,
|
||||
...drizzleBusinessListing,
|
||||
};
|
||||
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 type DrizzleUser = typeof users.$inferSelect;
|
||||
export type DrizzleBusinessListing = typeof businesses.$inferSelect;
|
||||
export type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
|
||||
|
||||
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];
|
||||
export function splitName(fullName: string): { firstname: string; lastname: string } {
|
||||
const parts = fullName.trim().split(/\s+/); // Teile den Namen am Leerzeichen auf
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
if (value instanceof Date) {
|
||||
res[key] = value;
|
||||
} else {
|
||||
flattenObject(value, res);
|
||||
}
|
||||
} else {
|
||||
res[key] = value;
|
||||
}
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
// Falls es nur ein Teil gibt, ist firstname und lastname gleich
|
||||
return { firstname: parts[0], lastname: parts[0] };
|
||||
} else {
|
||||
// Ansonsten ist der letzte Teil der lastname, der Rest der firstname
|
||||
const lastname = parts.pop()!;
|
||||
const firstname = parts.join(' ');
|
||||
return { firstname, lastname };
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
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 };
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "src/drizzle/import.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
|
|
@ -18,6 +18,6 @@
|
|||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"esModuleInterop":true
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,12 +27,17 @@
|
|||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"node_modules/quill/dist/quill.snow.css"
|
||||
"node_modules/quill/dist/quill.snow.css",
|
||||
"node_modules/leaflet/dist/leaflet.css"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
|
|
@ -41,7 +46,7 @@
|
|||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
|
|
@ -66,6 +71,17 @@
|
|||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
},
|
||||
"prod": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
|
|
@ -81,7 +97,9 @@
|
|||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {"proxyConfig": "proxy.conf.json"}
|
||||
"options": {
|
||||
"proxyConfig": "proxy.conf.json"
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
|
|
@ -102,7 +120,12 @@
|
|||
"src/assets",
|
||||
"cropped-Favicon-32x32.png",
|
||||
"cropped-Favicon-180x180.png",
|
||||
"cropped-Favicon-191x192.png"
|
||||
"cropped-Favicon-191x192.png",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "./node_modules/leaflet/dist/images",
|
||||
"output": "assets/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"prebuild": "node version.js",
|
||||
"build": "node version.js && ng build",
|
||||
"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",
|
||||
"test": "ng test",
|
||||
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs"
|
||||
|
|
@ -18,34 +19,41 @@
|
|||
"@angular/common": "^18.1.3",
|
||||
"@angular/compiler": "^18.1.3",
|
||||
"@angular/core": "^18.1.3",
|
||||
"@angular/fire": "^18.0.1",
|
||||
"@angular/forms": "^18.1.3",
|
||||
"@angular/platform-browser": "^18.1.3",
|
||||
"@angular/platform-browser-dynamic": "^18.1.3",
|
||||
"@angular/platform-server": "^18.1.3",
|
||||
"@angular/router": "^18.1.3",
|
||||
"@bluehalo/ngx-leaflet": "^18.0.2",
|
||||
"@fortawesome/angular-fontawesome": "^0.15.0",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@ng-select/ng-select": "^13.4.1",
|
||||
"@ngneat/until-destroy": "^10.0.0",
|
||||
"@stripe/stripe-js": "^4.3.0",
|
||||
"@types/cropperjs": "^1.3.0",
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"browser-bunyan": "^1.8.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"express": "^4.18.2",
|
||||
"flowbite": "^2.4.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"keycloak-angular": "^16.0.1",
|
||||
"keycloak-js": "^25.0.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"memoize-one": "^6.0.0",
|
||||
"ng-gallery": "^11.0.0",
|
||||
"ngx-currency": "^18.0.0",
|
||||
"ngx-image-cropper": "^8.0.0",
|
||||
"ngx-mask": "^18.0.0",
|
||||
"ngx-quill": "^26.0.5",
|
||||
"ngx-sharebuttons": "^15.0.3",
|
||||
"ngx-stripe": "^18.1.0",
|
||||
"on-change": "^5.0.1",
|
||||
"posthog-js": "^1.259.0",
|
||||
"quill": "2.0.2",
|
||||
"rxjs": "~7.8.1",
|
||||
"tslib": "^2.6.3",
|
||||
"urlcat": "^3.1.0",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:3000",
|
||||
"secure": false
|
||||
"/bizmatch": {
|
||||
"target": "http://localhost:3001",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"/pictures": {
|
||||
"target": "http://localhost:8080",
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,18 @@
|
|||
<!-- <div class="container"> -->
|
||||
<div [ngClass]="{ 'bg-slate-100': actualRoute !== 'home' }">
|
||||
@if (actualRoute !=='home' && actualRoute !=='pricing'){
|
||||
<div class="wrapper" [ngClass]="{ 'print:bg-white': actualRoute !== 'home' }">
|
||||
@if (actualRoute !=='home' && actualRoute !=='login' && actualRoute!=='emailVerification' && actualRoute!=='email-authorized'){
|
||||
<header></header>
|
||||
}
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</main>
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
|
||||
|
|
@ -30,26 +38,9 @@
|
|||
</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-search-modal></app-search-modal>
|
||||
<app-search-modal-commercial></app-search-modal-commercial>
|
||||
<app-confirmation></app-confirmation>
|
||||
<app-email></app-email>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,3 @@
|
|||
// .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 {
|
||||
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
|
||||
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
|
||||
import { filter } from 'rxjs/operators';
|
||||
import build from '../build';
|
||||
import { ConfirmationComponent } from './components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from './components/confirmation/confirmation.service';
|
||||
import { EMailComponent } from './components/email/email.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { HeaderComponent } from './components/header/header.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 { AuditService } from './services/audit.service';
|
||||
import { GeoService } from './services/geo.service';
|
||||
import { LoadingService } from './services/loading.service';
|
||||
import { UserService } from './services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, SearchModalCommercialComponent, ConfirmationComponent, EMailComponent],
|
||||
providers: [],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
|
|
@ -31,9 +34,10 @@ export class AppComponent {
|
|||
public loadingService: LoadingService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private keycloakService: KeycloakService,
|
||||
private userService: UserService,
|
||||
private confirmationService: ConfirmationService,
|
||||
private auditService: AuditService,
|
||||
private geoService: GeoService,
|
||||
) {
|
||||
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
|
||||
let currentRoute = this.activatedRoute.root;
|
||||
|
|
@ -47,17 +51,15 @@ export class AppComponent {
|
|||
ngOnInit() {}
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
// this.router.events.subscribe(event => {
|
||||
// if (event instanceof NavigationEnd) {
|
||||
// initFlowbite();
|
||||
// }
|
||||
// });
|
||||
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
|
||||
this.showVersionDialog();
|
||||
}
|
||||
}
|
||||
|
||||
showVersionDialog() {
|
||||
this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
|
||||
}
|
||||
isFilterRoute(): boolean {
|
||||
const filterRoutes = ['/businessListings', '/commercialPropertyListings', '/brokerListings'];
|
||||
return filterRoutes.includes(this.actualRoute);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
|
||||
import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core';
|
||||
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
|
||||
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
|
||||
import { getAuth, provideAuth } from '@angular/fire/auth';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
|
||||
import { GALLERY_CONFIG, GalleryConfig } from 'ng-gallery';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
import { provideNgxStripe } from 'ngx-stripe';
|
||||
import { environment } from '../environments/environment';
|
||||
import { customKeycloakAdapter } from '../keycloak';
|
||||
import { routes } from './app.routes';
|
||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
||||
import { KeycloakInitializerService } from './services/keycloak-initializer.service';
|
||||
import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
|
||||
import { GlobalErrorHandler } from './services/globalErrorHandler';
|
||||
import { POSTHOG_INIT_PROVIDER } from './services/posthog.factory';
|
||||
import { SelectOptionsService } from './services/select-options.service';
|
||||
import { createLogger } from './utils/utils';
|
||||
// provideClientHydration()
|
||||
|
|
@ -17,16 +24,6 @@ const logger = createLogger('ApplicationConfig');
|
|||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{ provide: KeycloakService },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
// useFactory: initializeKeycloak,
|
||||
//useFactory: initializeKeycloak,
|
||||
useFactory: initializeKeycloak3,
|
||||
multi: true,
|
||||
//deps: [KeycloakService],
|
||||
deps: [KeycloakInitializerService],
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initServices,
|
||||
|
|
@ -40,9 +37,29 @@ export const appConfig: ApplicationConfig = {
|
|||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
useClass: TimeoutInterceptor,
|
||||
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(
|
||||
routes,
|
||||
withEnabledBlockingInitialNavigation(),
|
||||
|
|
@ -51,7 +68,9 @@ export const appConfig: ApplicationConfig = {
|
|||
anchorScrolling: 'enabled',
|
||||
}),
|
||||
),
|
||||
...(environment.production ? [POSTHOG_INIT_PROVIDER] : []),
|
||||
provideAnimations(),
|
||||
provideNgxStripe('pk_test_IlpbVQhxAXZypLgnCHOCqlj8'),
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
|
|
@ -64,6 +83,9 @@ export const appConfig: ApplicationConfig = {
|
|||
],
|
||||
},
|
||||
}),
|
||||
provideFirebaseApp(() => initializeApp(environment.firebaseConfig)),
|
||||
provideAuth(() => getAuth()),
|
||||
// provideFirestore(() => getFirestore()),
|
||||
],
|
||||
};
|
||||
function initServices(selectOptions: SelectOptionsService) {
|
||||
|
|
@ -71,47 +93,3 @@ function initServices(selectOptions: SelectOptionsService) {
|
|||
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}`);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ import { Routes } from '@angular/router';
|
|||
import { LogoutComponent } from './components/logout/logout.component';
|
||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
|
||||
import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component';
|
||||
import { EmailVerificationComponent } from './components/email-verification/email-verification.component';
|
||||
import { LoginRegisterComponent } from './components/login-register/login-register.component';
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
||||
import { UserListComponent } from './pages/admin/user-list/user-list.component';
|
||||
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
|
||||
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
|
||||
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
|
||||
|
|
@ -18,6 +22,7 @@ import { EditCommercialPropertyListingComponent } from './pages/subscription/edi
|
|||
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
|
||||
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
|
||||
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
|
||||
import { SuccessComponent } from './pages/success/success.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
|
|
@ -54,6 +59,22 @@ export const routes: Routes = [
|
|||
canActivate: [ListingCategoryGuard],
|
||||
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
},
|
||||
// {
|
||||
// path: 'login/:page',
|
||||
// component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
// },
|
||||
{
|
||||
path: 'login/:page',
|
||||
component: 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
|
||||
{
|
||||
|
|
@ -113,7 +134,7 @@ export const routes: Routes = [
|
|||
{
|
||||
path: 'emailUs',
|
||||
component: EmailUsComponent,
|
||||
canActivate: [AuthGuard],
|
||||
// canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// Logout
|
||||
|
|
@ -128,5 +149,33 @@ export const routes: Routes = [
|
|||
path: 'pricing',
|
||||
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' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export abstract class BaseInputComponent implements ControlValueAccessor {
|
|||
@Input() label: string = '';
|
||||
// @Input() id: string = '';
|
||||
@Input() name: string = '';
|
||||
isTooltipVisible = false;
|
||||
constructor(protected validationMessagesService: ValidationMessagesService) {}
|
||||
ngOnInit() {
|
||||
this.subscription = this.validationMessagesService.messages$.subscribe(() => {
|
||||
|
|
@ -51,4 +52,9 @@ export abstract class BaseInputComponent implements ControlValueAccessor {
|
|||
this.validationMessage = this.validationMessagesService.getMessage(this.name);
|
||||
}
|
||||
setDisabledState?(isDisabled: boolean): void {}
|
||||
toggleTooltip(event: Event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.isTooltipVisible = !this.isTooltipVisible;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
</svg>
|
||||
@let confirmation = (confirmationService.confirmation$ | async);
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation.message }}</h3>
|
||||
@if(confirmation.buttons==='both'){
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation?.message }}</h3>
|
||||
@if(confirmation?.buttons==='both'){
|
||||
<button
|
||||
(click)="confirmationService.accept()"
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
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';
|
||||
}
|
||||
|
|
@ -1,20 +1,8 @@
|
|||
<div #_container class="container">
|
||||
<!-- <div
|
||||
*ngFor="let item of items"
|
||||
cdkDrag
|
||||
(cdkDragEnded)="dragEnded($event)"
|
||||
(cdkDragStarted)="dragStarted()"
|
||||
(cdkDragMoved)="dragMoved($event)"
|
||||
class="item"
|
||||
[class.animation]="isAnimationActive"
|
||||
[class.large]="item === 3"
|
||||
>
|
||||
Drag Item {{ item }}
|
||||
</div> -->
|
||||
<div *ngFor="let item of items" cdkDrag (cdkDragEnded)="dragEnded($event)" (cdkDragStarted)="dragStarted()" (cdkDragMoved)="dragMoved($event)" [class.animation]="isAnimationActive" class="grid-item item">
|
||||
<div class="image-box hover:cursor-pointer">
|
||||
<img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg shadow-md" />
|
||||
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md" (click)="imageToDelete.emit(item)">
|
||||
<img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg drop-shadow-custom-bg" />
|
||||
<div class="absolute top-2 right-2 bg-white rounded-full p-1 drop-shadow-custom-bg" (click)="imageToDelete.emit(item)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<div class="container mx-auto py-8 px-4 max-w-md">
|
||||
<div class="bg-white p-6 rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg text-center">
|
||||
<!-- Loading state -->
|
||||
<ng-container *ngIf="verificationStatus === 'pending'">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
<p class="text-gray-700">Verifying your email address...</p>
|
||||
</ng-container>
|
||||
|
||||
<!-- Success state -->
|
||||
<ng-container *ngIf="verificationStatus === 'success'">
|
||||
<div class="flex justify-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-green-600 mb-5">Your email has been verified</h2>
|
||||
<p class="text-gray-700 mb-4">You will be redirected to your account page in 5 seconds</p>
|
||||
<a routerLink="/account" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Go to Account Page Now </a>
|
||||
</ng-container>
|
||||
|
||||
<!-- Error state -->
|
||||
<ng-container *ngIf="verificationStatus === 'error'">
|
||||
<div class="flex justify-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-red-600 mb-3">Verification Failed</h2>
|
||||
<p class="text-gray-700 mb-4">{{ errorMessage }}</p>
|
||||
<a routerLink="/login" class="inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"> Return to Login </a>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-email-authorized',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule],
|
||||
templateUrl: './email-authorized.component.html',
|
||||
})
|
||||
export class EmailAuthorizedComponent implements OnInit {
|
||||
verificationStatus: 'pending' | 'success' | 'error' = 'pending';
|
||||
errorMessage: string | null = null;
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private http: HttpClient, private authService: AuthService, private userService: UserService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const oobCode = this.route.snapshot.queryParamMap.get('oobCode');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
const mode = this.route.snapshot.queryParamMap.get('mode');
|
||||
|
||||
if (mode === 'verifyEmail' && oobCode && email) {
|
||||
this.verifyEmail(oobCode, email);
|
||||
} else {
|
||||
this.verificationStatus = 'error';
|
||||
this.errorMessage = 'Invalid verification link';
|
||||
}
|
||||
}
|
||||
|
||||
private verifyEmail(oobCode: string, email: string): void {
|
||||
this.http.post<{ message: string; token: string }>(`${environment.apiBaseUrl}/bizmatch/auth/verify-email`, { oobCode, email }).subscribe({
|
||||
next: async response => {
|
||||
this.verificationStatus = 'success';
|
||||
|
||||
try {
|
||||
// Use the custom token from the server to sign in with Firebase
|
||||
await this.authService.signInWithCustomToken(response.token);
|
||||
|
||||
// Try to get user info
|
||||
try {
|
||||
const user = await this.userService.getByMail(email);
|
||||
console.log('User retrieved:', user);
|
||||
} catch (userError) {
|
||||
console.error('Error getting user:', userError);
|
||||
// Don't change verification status - it's still a success
|
||||
}
|
||||
|
||||
// Redirect to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/account']);
|
||||
}, 5000);
|
||||
} catch (authError) {
|
||||
console.error('Error signing in with custom token:', authError);
|
||||
// Keep success status for verification, but add warning about login
|
||||
this.errorMessage = 'Email verified, but there was an issue signing you in. Please try logging in manually.';
|
||||
}
|
||||
},
|
||||
error: err => {
|
||||
this.verificationStatus = 'error';
|
||||
this.errorMessage = err.error?.message || 'Verification failed';
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
|
||||
<div class="bg-white p-8 rounded drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg w-full max-w-md text-center">
|
||||
<h2 class="text-2xl font-bold mb-4">Email Verification</h2>
|
||||
<p class="mb-4">A verification email has been sent to your email address. Please check your inbox and click the link to verify your account.</p>
|
||||
<p>Once verified, please return to the application.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-email-verification',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './email-verification.component.html',
|
||||
})
|
||||
export class EmailVerificationComponent {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue