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:
2026-10-02 12:54:44 +02:00
parent 5503ce5fc5
commit 311153b3c3
26 changed files with 8873 additions and 894 deletions
+129
View File
@@ -0,0 +1,129 @@
/** PostgreSQL-Verbindung + Schema für das Mieterportal.
*
* Tabellen werden beim Start idempotent angelegt (CREATE TABLE IF NOT EXISTS),
* danach lädt seed.js die Demo-Daten, falls die Datenbank leer ist.
*/
const { Pool } = require('pg')
const pool = new Pool({
connectionString:
process.env.DATABASE_URL || 'postgres://mp:mp@localhost:5432/mieterportal',
max: 10,
})
const SCHEMA = `
CREATE TABLE IF NOT EXISTS units (
id TEXT PRIMARY KEY,
building TEXT NOT NULL,
number TEXT NOT NULL,
floor INT NOT NULL,
size NUMERIC(8,2) NOT NULL,
cold_rent NUMERIC(10,2) NOT NULL,
warm_rent NUMERIC(10,2) NOT NULL,
tenant TEXT,
status TEXT NOT NULL CHECK (status IN ('vermietet','frei'))
);
CREATE TABLE IF NOT EXISTS transactions (
id TEXT PRIMARY KEY,
unit_id TEXT NOT NULL REFERENCES units(id),
tenant TEXT NOT NULL,
month TEXT NOT NULL,
year INT NOT NULL,
amount NUMERIC(10,2) NOT NULL,
type TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('bezahlt','offen','überfällig')),
paid_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_transactions_unit ON transactions(unit_id);
CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY,
unit_id TEXT NOT NULL REFERENCES units(id),
tenant TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL,
priority TEXT NOT NULL CHECK (priority IN ('niedrig','mittel','hoch')),
status TEXT NOT NULL DEFAULT 'offen'
CHECK (status IN ('offen','in Bearbeitung','erledigt')),
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_tickets_unit ON tickets(unit_id);
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
unit_id TEXT REFERENCES units(id), -- NULL = Hausdokument für alle
title TEXT NOT NULL,
category TEXT NOT NULL,
size TEXT NOT NULL,
date TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS threads (
id TEXT PRIMARY KEY,
unit_id TEXT NOT NULL REFERENCES units(id),
tenant TEXT NOT NULL,
subject TEXT NOT NULL,
unread_by_admin BOOLEAN NOT NULL DEFAULT FALSE,
unread_by_tenant BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS idx_threads_unit ON threads(unit_id);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
from_role TEXT NOT NULL CHECK (from_role IN ('tenant','admin')),
author TEXT NOT NULL,
text TEXT NOT NULL,
at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id);
CREATE TABLE IF NOT EXISTS announcements (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
text TEXT NOT NULL,
date TEXT NOT NULL,
tag TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
benutzername TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
passwort_hash TEXT NOT NULL,
rolle TEXT NOT NULL CHECK (rolle IN ('verwaltung','mieter')),
email TEXT NOT NULL,
einheit_id TEXT REFERENCES units(id),
aktiv BOOLEAN NOT NULL DEFAULT TRUE,
erstellt_am TEXT NOT NULL,
letzter_login TEXT
);
CREATE TABLE IF NOT EXISTS registration_codes (
code TEXT PRIMARY KEY,
einheit_id TEXT REFERENCES units(id),
erstellt_am TEXT NOT NULL,
verwendet_am TEXT
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
`
async function init() {
await pool.query(SCHEMA)
}
/** Aufräumen abgelaufener Sessions (gelegentlich aufgerufen). */
async function sessionsAufraeumen() {
await pool.query('DELETE FROM sessions WHERE expires < now()')
}
module.exports = { pool, init, sessionsAufraeumen }