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:
@@ -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 }
|
||||
Reference in New Issue
Block a user