v1.3: Serverseitige Datenbank (PostgreSQL + Node-API) ersetzt localStorage
- server/: Express-API (Auth, Bootstrap, Tickets, Threads, Benutzerverwaltung, Codes) mit pg-Schema (11 Tabellen), idempotem Seed (50 Wohnungen, 552 Buchungen) - mp-db (PostgreSQL 16, Volume mp_pgdata) + mp-api im Compose-Stack, nginx proxiet /api/ an mp-api; deploy.sh baut API-Image auf dem Server - Frontend: Store laedt /api/bootstrap, Schreibzugriff async, kein localStorage mehr (ausser Theme), Ladebildschirm beim Session-Check, vite-Proxy fuer Dev - Rollenrechte serverseitig: Mieter nur eigene Wohnung (getestet, 403-Verweis) - Mail-Benachrichtigungen komplett serverseitig (server/src/mail.js) - beispieldaten.py schreibt server/seed/generated.json; Verwaltungs-Mail = axel@hocks.eu - E2E live verifiziert: Login, Scoping, Ticket + Zustellung (status=sent), Registrierung, Codes; Testdaten bereinigt; Doku nachgefuehrt
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Kimi-Kontext: Hinterstein Immobilien · Mieterportal (mieterportal.hocks.eu)
|
||||
|
||||
> Projekt-Kontextdatei für Kimi Work. Stand: 02.10.2026, 14:30 Uhr.
|
||||
> Projekt-Kontextdatei für Kimi Work. Stand: 02.10.2026, 13:00 Uhr.
|
||||
> Bitte bei größeren Änderungen am Projekt aktualisieren.
|
||||
> Konventionen und Workflow sind am Projekt Schulportal (sfg.hocks.eu) orientiert.
|
||||
|
||||
@@ -10,15 +10,18 @@ Mieterportal der **Hinterstein Immobilien** (Demo, Inhaber Joseph Hinterstein) f
|
||||
**50 Wohnungen in 3 Häusern** (Hauptstraße 12 · Ringweg 3 · Am Park 5). Analog zum
|
||||
Schulportal: alle Mietangelegenheiten digital an einem Ort.
|
||||
|
||||
**Authentifizierung (echtes Login seit v1.1):** Startseite `/login` mit
|
||||
Login-Pflicht (Route-Guard `RequireAuth` in `App.tsx` leitet unangemeldete Besucher
|
||||
auf `/login` um). **Gruppen:** `verwaltung` und `mieter`. Mieter können sich über
|
||||
einen **Registrierungscode** selbst registrieren (`/registrieren`); Codes legt die
|
||||
Verwaltung in der Benutzerverwaltung an (einmalig nutzbar, Wohnungsbezug).
|
||||
Passwörter als **SHA-256-Hash** in localStorage, Session 8 h
|
||||
(Key `mp_benutzer_session`). Demo-Zugänge (Passwort `0815`):
|
||||
`j.hinterstein` (Verwaltung, Joseph Hinterstein), `a.berger` (Mieterin,
|
||||
Hauptstraße 12 Whg. 7), `m.wolf` (Mieter). Demo-Registrierungscode: `MP-2026-0815`.
|
||||
**Datenhaltung (seit v1.3):** zentral serverseitig in **PostgreSQL** über eine
|
||||
**Node-API** (Express) – kein localStorage mehr für Mietdaten. Mehrere Nutzer
|
||||
sehen denselben Datenstand in Echtzeit; Rechte werden serverseitig erzwungen
|
||||
(Mieter nur eigene Wohnung, Verwaltung alles).
|
||||
|
||||
**Authentifizierung:** Login-Pflicht auf `/login` (Route-Guard in `App.tsx`).
|
||||
Gruppen `verwaltung` und `mieter`; Mieter registrieren sich per einmaligem
|
||||
**Registrierungscode** (`/registrieren`). Passwörter als SHA-256-Hash (im Browser
|
||||
gerechnet, Vergleich serverseitig), Sitzung als HttpOnly-Cookie `mp_session`
|
||||
(8 h, Tabelle `sessions`). Demo-Zugänge (Passwort `0815`): `j.hinterstein`
|
||||
(Verwaltung), `a.berger` (Mieterin, Hauptstraße 12 Whg. 7), `m.wolf` (Mieter,
|
||||
Ringweg 3 Whg. 4). Demo-Registrierungscode: `MP-2026-0815`.
|
||||
|
||||
**Die 6 Module:**
|
||||
|
||||
@@ -43,12 +46,14 @@ Hauptstraße 12 Whg. 7), `m.wolf` (Mieter). Demo-Registrierungscode: `MP-2026-08
|
||||
| Teil | Technologie |
|
||||
|---|---|
|
||||
| Frontend | React 18 + TypeScript, Vite 7, Tailwind v3.4 + shadcn/ui (Radix), React Router 7 (BrowserRouter), Recharts |
|
||||
| State | React Context + useReducer, Persistenz in localStorage (`hinterstein-mieterportal-v2`) |
|
||||
| Daten | `src/data/seed.ts` (handschriftlich) + `src/data/generated.json` (maschinell via `beispieldaten.py`) |
|
||||
| Auth | `src/lib/auth.ts` (SHA-256, Session 8 h, Keys `mp_benutzer`/`mp_benutzer_session`), Route-Guard in `App.tsx` |
|
||||
| State | Store-Kontext im Browser, **alle Daten serverseitig** (kein localStorage mehr); Ladezustand `bereit`, Schreibzugriff async |
|
||||
| API | `server/`: Node 20 + Express + pg, Session-Cookie, Rollenrechte, serverseitige Mail-Benachrichtigungen |
|
||||
| Datenbank | PostgreSQL 16 (`mp-db`), Schema/Seed in `server/src/db.js` + `seed.js`, Volume `mp_pgdata` |
|
||||
| Daten | `server/seed/generated.json` (50 Wohnungen + 552 Buchungen via `beispieldaten.py`) + `server/src/seed-data.js` (handschriftlich) |
|
||||
| Auth | SHA-256-Hash (Browser) + HttpOnly-Cookie-Session (8 h), `src/lib/api.ts`, `src/lib/auth.ts` |
|
||||
| Branding | Kunde: `src/assets/hinterstein/` (logo.png, joseph-hinterstein.jpg, kontakt.jpg); KI-generiert: `src/assets/logo.png`, `hero.jpg`, `modules.jpg` |
|
||||
| Webserver (Ziel) | Docker-Container `mp-portal` (nginx), statisches `dist/`, gemountet `/opt/mieterportal/dist` |
|
||||
| Mail | `mail-server/`: Postfix-Relay (`mp-mail`) + Python-API (`mp-mail-api`) – **live**, `/opt/mp-mail` auf dem Server; **verkabelt**: Portal-nginx proxyt `POST /api/mail/send` → `mp-mail-api:8080` und injiziert das Token aus `portal.env` (serverseitig, nie im Browser/Repo) |
|
||||
| Webserver | Docker-Container `mp-portal` (nginx), statisches `dist/`, proxiet `/api/` → `mp-api:3000` |
|
||||
| Mail | `mail-server/`: Postfix-Relay (`mp-mail`) + Python-API (`mp-mail-api`) – **live**, `/opt/mp-mail`; Portal-nginx proxyt `POST /api/mail/send` mit Token aus `portal.env` |
|
||||
| Server | Root-VPS hocks.eu (IONOS), Docker + Portainer, Subdomain via NPM (Nginx Proxy Manager, Let's Encrypt) |
|
||||
|
||||
## 3. Wichtige Dateien
|
||||
@@ -59,23 +64,26 @@ ausgesperrt via `.gitignore` (`mail-server/mail.env` etc.).
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| `src/store/store.tsx` | Store-Provider, Reducer, Actions (setRole/addTicket/setTicketStatus/addMessage/newThread/markThreadRead), `fmtEUR` |
|
||||
| `src/data/seed.ts` | Tickets, Dokumente, Nachrichten-Threads, Ankündigungen + Re-Export units/transactions aus generated.json |
|
||||
| `src/data/generated.json` | 50 Wohnungen + 552 Buchungen, erzeugt von `beispieldaten.py` (Seed 20261002) |
|
||||
| `beispieldaten.py` | Deterministischer Generator: `python beispieldaten.py [seed]` |
|
||||
| `src/types/index.ts` | Alle Typen (Unit, Transaction, Ticket, DocumentItem, Thread, Announcement) |
|
||||
| `src/components/AppLayout.tsx` | Sidebar (Desktop) + Mobile Top-Bar/Nav, Rollenwechsel, Ungelesen-Badge, ThemeToggle |
|
||||
| `src/store/store.tsx` | Store-Kontext: lädt `/api/bootstrap`, stellt Collections + `me`/`role`/`meineEinheit` bereit, async-Methoden für alle Schreibaktionen, `fmtEUR` |
|
||||
| `src/lib/api.ts` | Fetch-Client (same-origin, JSON, `ApiFehler` mit deutscher Meldung) |
|
||||
| `src/lib/auth.ts` | SHA-256 für Passwörter, `DEMO_PASSWORT` |
|
||||
| `server/src/index.js` | Express-API: Auth (Login/Logout/me/Register), Bootstrap, Tickets, Threads/Messages, Benutzerverwaltung, Codes, `/api/healthz` |
|
||||
| `server/src/db.js` | pg-Pool + idempotentes Schema (11 Tabellen, snake_case) |
|
||||
| `server/src/seed.js` | Idempotenter Seed (läuft nur bei leerer users-Tabelle) |
|
||||
| `server/src/seed-data.js` | Handschriftliche Demo-Daten (Tickets, Dokumente, Threads, Ankündigungen, Benutzer, Codes) |
|
||||
| `server/src/mail.js` | Serverseitiger Versand über mp-mail-api (fire-and-forget) |
|
||||
| `server/seed/generated.json` | 50 Wohnungen + 552 Buchungen, erzeugt von `beispieldaten.py` (Seed 20261002) |
|
||||
| `beispieldaten.py` | Deterministischer Generator: `python beispieldaten.py [seed]` → `server/seed/generated.json` |
|
||||
| `src/types/index.ts` | Alle Typen (Unit, Transaction, Ticket, DocumentItem, Thread, Announcement, Benutzer, RegistrierungsCode) |
|
||||
| `src/components/AppLayout.tsx` | Sidebar (Desktop) + Mobile Top-Bar/Nav, Logout, Ungelesen-Badge, ThemeToggle |
|
||||
| `src/components/ThemeToggle.tsx` | Dark-Mode-Umschalter (Sonne/Mond) |
|
||||
| `src/lib/theme.ts` | `mp-theme` in localStorage, Init vor erstem Render (main.tsx) |
|
||||
| `src/pages/Login.tsx` | Login-Pflicht: Hero mit Joseph-Hinterstein-Portrait, Logo, Feature-Liste, Login-Formular + Registrierung-Link + Demo-Einstiege |
|
||||
| `src/lib/mail.ts` | E-Mail-Benachrichtigungen: `sendeBenachrichtigung` (fire-and-forget) an `/api/mail/send`, Empfänger-Auflösung (Verwaltung / Mieter je Wohnung) |
|
||||
| `docker/templates/default.conf.template` | nginx-Site als envsubst-Template; proxiet `POST /api/mail/send` → `mp-mail-api:8080` mit `${MAIL_API_TOKEN}` aus `portal.env` |
|
||||
| `src/pages/Benutzer.tsx` | Benutzerverwaltung (nur Verwaltung): Nutzer, Gruppen, Registrierungscodes |
|
||||
| `Dockerfile` | Optionaler All-in-One-Build (node build → nginx) für `docker build`-Workflow |
|
||||
| `docker/nginx.conf` | nginx-Site: SPA-Fallback, Gzip, Cache-Header für /assets/, Security-Header |
|
||||
| `docker-compose.yml` | `mp-portal` (nginx + gemountetes dist) im externen Netz `npm_default`; Mail-Stack separat |
|
||||
| `deploy.sh` | Bewährter Deploy-Workflow (s. Kap. 4) |
|
||||
| `mail-server/` | Postfix-Relay + schlanke Mail-API (Muster Schulportal, angepasst) |
|
||||
| `src/lib/theme.ts` | `mp-theme` in localStorage (nur noch Theme!), Init vor erstem Render (main.tsx) |
|
||||
| `src/pages/Login.tsx` | Login-Pflicht: Hero mit Joseph-Hinterstein-Portrait, Logo, Feature-Liste, Login + Registrierung + Demo-Einstiege |
|
||||
| `src/pages/BenutzerVerwaltung.tsx` | Benutzerverwaltung (nur Verwaltung): Nutzer anlegen/pflegen, Registrierungscodes erzeugen |
|
||||
| `docker/templates/default.conf.template` | nginx-Site als envsubst-Template: SPA, `/api/` → mp-api, `POST /api/mail/send` → mp-mail-api mit `${MAIL_API_TOKEN}` |
|
||||
| `docker-compose.yml` | Stack: `mp-portal` (nginx), `mp-api` (Node, Build ./server), `mp-db` (PostgreSQL 16, Volume mp_pgdata), Netz npm_default |
|
||||
| `deploy.sh` | Deploy-Workflow: Build, Paket (dist + compose + nginx + server), Upload, `docker compose up -d --build`, Health-Check |
|
||||
| `mail-server/` | Postfix-Relay + schlanke Mail-API (Muster Schulportal, angepasst) – separater Stack unter /opt/mp-mail |
|
||||
|
||||
## 4. Deploy-Workflow (bewährt, aus Schulportal übernommen)
|
||||
|
||||
@@ -103,10 +111,18 @@ tar -czf mp-dist.tar.gz -C dist .
|
||||
```
|
||||
|
||||
- SSH: Port **38592**, Key `Z:\Devel\ssh\priv_ionos_hocks_vps.ppk` (Pageant), Hostkey-Flag zwingend.
|
||||
- **`portal.env` auf dem Server wird vom compose als `env_file` gebraucht**
|
||||
(liefert `MAIL_API_TOKEN` für den nginx-envsubst). Einmalig angelegt:
|
||||
- **`portal.env` auf dem Server** (gitignored, liegt nur dort): liefert
|
||||
`MAIL_API_TOKEN` (für nginx-envsubst und mp-api) und `POSTGRES_PASSWORD`
|
||||
(für mp-db und die DATABASE_URL des mp-api). Einmalig angelegt:
|
||||
`grep '^MAIL_API_TOKEN=' /opt/mp-mail/mail.env > /opt/mieterportal/portal.env`
|
||||
Liegt nur auf dem Server (gitignored), wird von deploy.sh nicht hochgeladen.
|
||||
plus zufälliges `POSTGRES_PASSWORD=…`. Wichtig: compose interpoliert
|
||||
`${VARIABLE}` nur aus einer `.env`-Datei im Projektverzeichnis (nicht aus
|
||||
`env_file:`!) – daher auf dem Server `/opt/mieterportal/.env` als
|
||||
**Symlink auf portal.env**:
|
||||
`cd /opt/mieterportal && ln -sf portal.env .env`
|
||||
- deploy.sh macht `docker compose up -d --build` (baut das mp-api-Image auf
|
||||
dem Server, npm ci im Container) und prüft danach Container-Status,
|
||||
Portal-Hash und `GET /api/healthz`.
|
||||
- **Vorsicht Fail2Ban:** zu viele gescheiterte SSH-Versuche von einer IP führen zu einem
|
||||
Ban (Timeout-Verbindungsbild). Ban aufheben auf dem Server: `fail2ban-client unban --all`.
|
||||
- **Live (seit 02.10.2026):** Container `mp-portal` läuft auf hocks.eu, statisches `dist/`
|
||||
@@ -123,26 +139,25 @@ tar -czf mp-dist.tar.gz -C dist .
|
||||
## 5. Zugänge & Konfiguration
|
||||
|
||||
- **Demo-Zugänge (Passwort `0815`):** `j.hinterstein` (Verwaltung), `a.berger`
|
||||
(Mieterin, Hauptstraße 12 Whg. 7), `m.wolf` (Mieter). Demo-Registrierungscode:
|
||||
`MP-2026-0815`. Passwörter werden als SHA-256-Hash gespeichert; Store-Reset über
|
||||
Browser-DevTools (localStorage-Keys `mp_benutzer`, `mp_benutzer_session`,
|
||||
`hinterstein-mieterportal-v2` löschen).
|
||||
(Mieterin, Hauptstraße 12 Whg. 7), `m.wolf` (Mieter, Ringweg 3 Whg. 4).
|
||||
Demo-Registrierungscode: `MP-2026-0815`. Passwörter liegen als SHA-256-Hash
|
||||
in der users-Tabelle; Sitzung als HttpOnly-Cookie (Tabelle sessions).
|
||||
Abmelden reicht zum „Reset" – nichts liegt mehr im Browser-localStorage
|
||||
(außer dem Dark-Mode-Theme).
|
||||
- **Verwaltungs-E-Mail:** `axel@hocks.eu` (echtes, zustellbares Postfach, damit
|
||||
Benachrichtigungen ankommen) – im Portal änderbar.
|
||||
- **Mail-API-Token:** bewusst nicht hier dokumentiert – liegt auf dem Server in
|
||||
`/opt/mp-mail/mail.env` (Vorlage `mail.env.example` im Repo).
|
||||
- **Mail-Stack live (seit 02.10.2026):** Container `mp-mail` + `mp-mail-api` laufen
|
||||
auf hocks.eu unter `/opt/mp-mail` (Relay über `hocks-eu.mail.protection.outlook.com`).
|
||||
Testmail und API-`/send` (mit `X-Api-Token`) verifiziert (status=sent); ohne Token 401.
|
||||
- **Mail-Benachrichtigungen im Portal (seit 02.10., v1.2):** Der Portal-nginx
|
||||
(`docker/templates/default.conf.template`, envsubst) proxiet ausschließlich
|
||||
`POST /api/mail/send` an `http://mp-mail-api:8080/send` und setzt das Token
|
||||
aus `/opt/mieterportal/portal.env` (chmod 600, **nicht im Repo**, gitignored).
|
||||
Das Frontend (`src/lib/mail.ts`) sendet fire-and-forget: neue Schadensmeldung →
|
||||
alle aktiven Verwaltungsbenutzer, neue Nachricht/Antwort → Gegenstelle
|
||||
(Mieter ↔ Verwaltung, Empfänger über Wohnungsbezug). E2E verifiziert:
|
||||
POST über `https://mieterportal.hocks.eu/api/mail/send` → `versandt: true`
|
||||
(Test an axel@hocks.eu); GET wird mit 403 blockiert. Demo-Empfängeradressen
|
||||
(z. B. joseph@hinterstein-immo.de) sind fiktiv → dort kein Empfang möglich,
|
||||
echte Adressen in der Benutzerverwaltung pflegen.
|
||||
- **Mail-Benachrichtigungen (seit v1.2 nginx-Proxy, seit v1.3 serverseitig):** Der
|
||||
Portal-nginx proxiet `POST /api/mail/send` an `http://mp-mail-api:8080/send` mit
|
||||
Token aus `portal.env` (403 für alles außer POST). **Versand aus der Node-API**
|
||||
(`server/src/mail.js`, fire-and-forget): neue Schadensmeldung → alle aktiven
|
||||
Verwaltungsbenutzer, neue Nachricht/Antwort → Gegenstelle (Mieter ↔ Verwaltung,
|
||||
Auflösung über Wohnungsbezug). E2E verifiziert: Zustellung an axel@hocks.eu
|
||||
mit Postfix status=sent (02.10., 10:42 Uhr).
|
||||
|
||||
## 6. Konventionen & Besonderheiten
|
||||
|
||||
@@ -160,8 +175,12 @@ tar -czf mp-dist.tar.gz -C dist .
|
||||
## 7. Testing
|
||||
|
||||
- `npm run build` (`tsc -b`) ist die echte Typprüfung; `npx tsc --noEmit` prüft wegen
|
||||
Solution-tsconfig ggf. nichts.
|
||||
- Headless-Screenshots (Dark Mode = Systempräferenz):
|
||||
Solution-tsconfig ggf. nichts. Server-Syntax: `node --check server/src/*.js`.
|
||||
- API-Livetests mit curl (Cookie-Jar): Login → `/api/auth/me` → `/api/bootstrap`
|
||||
(Mieter-Sicht zählt kleiner!) → Ticket/Thread schreiben → Postfix-Log auf
|
||||
Zustellung prüfen (`docker logs mp-mail | grep to=<empfaenger>`).
|
||||
- Headless-Screenshots (Dark Mode = Systempräferenz, `--virtual-time-budget=10000`
|
||||
wartet auf den Session-Check):
|
||||
`"/c/Program Files/Google/Chrome/Application/chrome.exe" --headless --disable-gpu --screenshot=... --window-size=WxH "URL"`
|
||||
- Screenshots ausgeliefert nach `preview/`.
|
||||
|
||||
@@ -196,12 +215,28 @@ angeschlossen: nginx-Proxy `/api/mail/send` mit serverseitig injiziertem Token
|
||||
neuen Schadensmeldungen (→ Verwaltung) und Nachrichten/Antworten (→ Gegenstelle).
|
||||
E2E-Versand live verifiziert (versandt: true), GET auf den Endpunkt 403.
|
||||
|
||||
**Erledigt 02.10. (v1.3 live):** Serverseitige Datenbank: PostgreSQL 16 (`mp-db`,
|
||||
Volume `mp_pgdata`) + Node-API (`mp-api`, Express + pg, eigenes Image via
|
||||
deploy.sh `--build`). Schema 11 Tabellen (idempotent), Seed einmalig aus
|
||||
`server/seed/generated.json` + `seed-data.js` (Verwaltungs-E-Mail auf echtes
|
||||
Postfach axel@hocks.eu gesetzt). Frontend komplett auf API umgestellt:
|
||||
`src/lib/api.ts`, Store lädt `/api/bootstrap`, kein localStorage mehr (außer
|
||||
Theme), Ladebildschirm während Session-Check. Rollenrechte serverseitig
|
||||
durchgesetzt und getestet (Mieterin sieht nur hs-7: 12 Buchungen/2 Tickets,
|
||||
403 auf fremde Unterhaltung; Verwaltung sieht alles: 552 Buchungen/8 Tickets).
|
||||
Registrierung, Code-Erzeugung, Ticket inkl. Mail-Benachrichtigung (status=sent
|
||||
an axel@hocks.eu) E2E verifiziert; Testdaten danach bereinigt. deploy.sh baut
|
||||
das API-Image auf dem Server; `.env`-Symlink auf portal.env für
|
||||
Compose-Interpolation beachten (POSTGRES_PASSWORD!).
|
||||
|
||||
**Offen:**
|
||||
- Zertifikatserneuerung prüfen (per 01.12.2026): NPM-Renewer sollte cert 9 über HTTP-Challenge
|
||||
erneuern; sonst certbot/webroot + Kopie nach npm-9 wiederholen.
|
||||
- Demo-Empfängeradressen durch echte ersetzen, sobald bekannt.
|
||||
- Mögliche nächste Schritte: serverseitige Datenbank (PostgreSQL/Node-API) statt localStorage,
|
||||
Mehrseitigkeit für 50 echte Mietverhältnisse.
|
||||
- Demo-Empfängeradressen der Mieter (example.de) durch echte ersetzen, sobald bekannt.
|
||||
- Sicherheit für Produktion: Passwort-Hash serverseitig salten (bcrypt/argon2), CSRF-Token,
|
||||
Rate-Limiting auf /api/auth/login.
|
||||
- Mögliche nächste Schritte: Datenbank-Backup-Job (nächtlicher pg_dump), E-Mail-Verifizierung
|
||||
bei Registrierung, dokumenten-Download mit echten Dateien.
|
||||
|
||||
## 9. Sonstiges
|
||||
|
||||
|
||||
Reference in New Issue
Block a user