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
+100
View File
@@ -0,0 +1,100 @@
/** Idempotenter Seed: füllt die Datenbank beim ersten Start mit den
* Demo-Daten (50 Wohnungen, 552 Buchungen, Tickets, Dokumente, Threads,
* Ankündigungen, Demo-Benutzer, Registrierungscode). Läuft nur, wenn die
* users-Tabelle leer ist – bestehende Daten werden nie überschrieben.
*/
const { pool } = require('./db')
const { users, registrationCodes, tickets, documents, threads, announcements } = require('./seed-data')
const generated = require('../seed/generated.json')
async function seed() {
const { rows } = await pool.query('SELECT COUNT(*)::int AS n FROM users')
if (rows[0].n > 0) return false
const client = await pool.connect()
try {
await client.query('BEGIN')
for (const u of generated.units) {
await client.query(
`INSERT INTO units (id, building, number, floor, size, cold_rent, warm_rent, tenant, status)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
[u.id, u.building, u.number, u.floor, u.size, u.coldRent, u.warmRent, u.tenant, u.status],
)
}
for (const t of generated.transactions) {
await client.query(
`INSERT INTO transactions (id, unit_id, tenant, month, year, amount, type, status, paid_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
[t.id, t.unitId, t.tenant, t.month, t.year, t.amount, t.type, t.status, t.paidAt ?? null],
)
}
// Mieter-Namen in Tickets/Threads aus der units-Tabelle nachziehen
const { rows: unitRows } = await client.query('SELECT id, tenant FROM units')
const mieterVon = Object.fromEntries(unitRows.map((u) => [u.id, u.tenant]))
for (const t of tickets) {
await client.query(
`INSERT INTO tickets (id, unit_id, tenant, title, description, category, priority, status, created_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
[t.id, t.unit_id, mieterVon[t.unit_id] ?? t.tenant, t.title, t.description, t.category, t.priority, t.status, t.created_at],
)
}
for (const d of documents) {
await client.query(
`INSERT INTO documents (id, unit_id, title, category, size, date) VALUES ($1,$2,$3,$4,$5,$6)`,
[d.id, d.unit_id, d.title, d.category, d.size, d.date],
)
}
for (const th of threads) {
await client.query(
`INSERT INTO threads (id, unit_id, tenant, subject, unread_by_admin, unread_by_tenant)
VALUES ($1,$2,$3,$4,$5,$6)`,
[th.id, th.unit_id, mieterVon[th.unit_id] ?? th.tenant, th.subject, th.unread_by_admin, th.unread_by_tenant],
)
for (const m of th.messages) {
await client.query(
`INSERT INTO messages (id, thread_id, from_role, author, text, at) VALUES ($1,$2,$3,$4,$5,$6)`,
[m.id, th.id, m.from_role, m.author, m.text, m.at],
)
}
}
for (const a of announcements) {
await client.query(
`INSERT INTO announcements (id, title, text, date, tag) VALUES ($1,$2,$3,$4,$5)`,
[a.id, a.title, a.text, a.date, a.tag],
)
}
for (const b of users) {
await client.query(
`INSERT INTO users (id, benutzername, name, passwort_hash, rolle, email, einheit_id, aktiv, erstellt_am, letzter_login)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[b.id, b.benutzername, b.name, b.passwort_hash, b.rolle, b.email, b.einheit_id, b.aktiv, b.erstellt_am, b.letzter_login ?? null],
)
}
for (const c of registrationCodes) {
await client.query(
`INSERT INTO registration_codes (code, einheit_id, erstellt_am, verwendet_am) VALUES ($1,$2,$3,$4)`,
[c.code, c.einheit_id, c.erstellt_am, c.verwendet_am],
)
}
await client.query('COMMIT')
return true
} catch (e) {
await client.query('ROLLBACK')
throw e
} finally {
client.release()
}
}
module.exports = { seed }