docs(PROJ-11): TLS infra done, end-to-end verified over public domains

Nginx Proxy Manager now terminates TLS for matrix.perlbach24.de and
talkhub.perlbach24.de with real Let's Encrypt certs. talkhub's backend
.env updated (SYNAPSE_PUBLIC_BASE_URL, APP_BASE_URL). Full chat flow
re-verified end-to-end against the public HTTPS domains: login, token
mint, room resolve, message send — all 200s, event id returned.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-11 22:28:33 +02:00
1 parent 8f6e05481b
commit c200fafd03
1 file changed
+121 -2
+121 -2
View File
@@ -115,8 +115,10 @@ Kein neuer Datenspeicher in der TalkTime-Datenbank nötig — Mitglied-zu-Matrix
- **Frontend:** `matrix-js-sdk` — offizielle Matrix-Client-Bibliothek, übernimmt Verbindungsaufbau, Nachrichtenabgleich, Lesebestätigungen
- **Backend:** keine neuen Pakete — nutzt die bereits vorhandene Synapse-Anbindung aus [[PROJ-9]]
### Offene Infrastruktur-Voraussetzung
Damit der Browser Synapse direkt erreichen kann, muss der Matrix-Server auch außerhalb des internen Netzes (bzw. für alle Nutzer-Endgeräte) erreichbar sein, inklusive TLS — aktuell (Testaufbau) läuft Synapse nur auf der internen LXC-IP ohne TLS. Das ist eine Deployment-Voraussetzung für den produktiven Einsatz dieses Features, keine Backend-/Frontend-Codeänderung.
### Infrastruktur-Voraussetzung — ERLEDIGT (2026-09-11)
~~Damit der Browser Synapse direkt erreichen kann, muss der Matrix-Server auch außerhalb des internen Netzes erreichbar sein, inklusive TLS.~~ Erledigt: Nginx Proxy Manager (192.168.66.9) terminiert TLS für `matrix.perlbach24.de` → `192.168.66.7:8008` und `talkhub.perlbach24.de` → `192.168.66.8:80`, beide mit echten Let's-Encrypt-Zertifikaten. `backend/.env` auf `talkhub` zeigt jetzt `SYNAPSE_PUBLIC_BASE_URL=https://matrix.perlbach24.de` und `APP_BASE_URL=https://talkhub.perlbach24.de`.
Kompletter Flow end-to-end über die öffentlichen TLS-Domains verifiziert: Login → `POST /api/chat/token` (liefert `homeserverUrl: https://matrix.perlbach24.de`) → `GET /api/chat/room` → Nachricht per `PUT .../send/m.room.message` direkt an `https://matrix.perlbach24.de` gesendet, HTTP 200, Event-ID zurückbekommen. Damit ist auch **BUG-1 sinngemäß behoben** (Backend läuft mit korrekten Werten) — die `.env.example`-Dokumentation selbst fehlt aber weiterhin (Permission-Sperre, siehe oben), das ist nur noch ein Doku-Nachtrag, keine funktionale Lücke mehr.
---
@@ -275,3 +277,120 @@ Nebenbei behoben: `createMatrixClient()` lädt `matrix-js-sdk` jetzt per dynamis
**Nicht in diesem Pass behoben (niedrigere Priorität laut QA):** BUG-5 (Polling-Backoff), BUG-6 (History-Visibility-Migration für Bestandsräume), BUG-7 (Bundle-Größe — durch den BUG-4-Fix bereits spürbar verbessert, aber nicht Ziel dieses Fixes), BUG-8 (Helmet/CORS — pre-existing aus PROJ-1, nicht PROJ-11-Scope), BUG-9/BUG-10.
Empfehlung: erneut `/qa` laufen lassen, sobald Synapse per TLS von außen erreichbar ist und BUG-1 (Env-Doku) manuell nachgezogen wurde.
---
## QA Re-Test (2026-09-11, Runde 2)
**Getestet gegen:** commit `8f6e054` (fix(PROJ-11): address QA BUG-2/3/4, partially mitigate BUG-1)
**Testart:** Code-Verifikation der Fixes, Backend-/Frontend-Typecheck, Production-Build (Bundle-Messung), Live-Probe gegen `matrix.perlbach24.de`, Red-Team-Review der neu hinzugekommenen Datenpfade.
**Einschränkung:** `matrix.perlbach24.de` antwortet aktuell mit **HTTP 502 (openresty)** — weder `/_matrix/client/versions` noch `user_directory/search` erreichbar. Ein Live-Funktionstest des Chats sowie die Verifikation der BUG-2-Mitigation auf der laufenden Instanz waren daher erneut nicht möglich. Cross-Browser/Responsive weiterhin nur statisch bewertet.
### Status der Vorrunden-Bugs
| Bug | Vorher | Jetzt | Bewertung |
|-----|--------|-------|-----------|
| BUG-1 (Critical) Env-Doku | offen | **weiterhin offen** | Nicht in `8f6e054` enthalten. `backend/.env.example` liegt auch für mich außerhalb der Permission-Sandbox (Read + `git diff` beide verweigert) — ich kann den aktuellen Inhalt **nicht verifizieren**. Im Working Tree ist die Datei als `M` markiert, d.h. es gibt eine uncommittete lokale Änderung. Falls das der Fix ist: **er ist nicht committet** und geht bei jedem Clone/Deployment verloren. Bleibt P0. |
| BUG-2 (High) Tenant-Isolation | offen | **teilgemildert, wie deklariert** | `synapse/homeserver.yaml.sample` enthält `user_directory.enabled: false` samt ehrlicher Begründung. Live-Verifikation nicht möglich (502). Restrisiko (direktes Anschreiben bei bekannter Matrix-ID) korrekt als offene Architekturentscheidung dokumentiert statt als „fixed" deklariert — gutes Vorgehen. Bleibt offen, jetzt aber als bewusste, dokumentierte Entscheidung. |
| BUG-3 (High) Kick-Handling | offen | **BEHOBEN** | `use-team-chat.ts:135-143` hört auf `RoomEvent.MyMembership`, filtert korrekt auf den eigenen `roomId`, setzt `status="removed"`; `TeamChat.tsx` rendert dafür einen eigenen Alert-Zustand vor dem `ready`-Pfad, Verlauf + Composer verschwinden. Korrekt. |
| BUG-4 (High) Ungelesen-Zähler | offen | **BEHOBEN (mit Folgefehlern)** | `use-team-unread-counts.ts` + Badge auf jeder Kachel in `TeamsPage.tsx:173-180`, `matrixRoomId` kommt aus `GET /api/teams`. Akzeptanzkriterium 6 damit erfüllt. Die Umsetzung bringt aber vier neue Befunde, siehe BUG-11 bis BUG-14. |
| BUG-7 (Medium) Bundle | offen | **NICHT verbessert, in einem Punkt verschlechtert** | siehe BUG-11 |
| BUG-5/6/8/9/10 | offen | offen (bewusst nicht adressiert) | unverändert gültig |
Typechecks `backend` und `frontend` (`tsc --noEmit`) laufen sauber. Production-Build erfolgreich.
### Neue Bugs aus dem Bugfix-Pass
**BUG-11 (High) — matrix-js-sdk lädt jetzt schon beim Öffnen der Teams-Liste statt erst beim Chat**
Build-Messung bestätigt die Chunk-Aufteilung (`TeamDetail` 853 kB → 27 kB, SDK separat in `browser-index-*.js` 829,59 kB / 229 kB gzip + `matrix_sdk_crypto_wasm_bg-*.wasm` 7.833 kB). Der Effekt ist aber praktisch aufgehoben: `useTeamUnreadCounts` wird in `TeamsPage` **unbedingt** gemountet und ruft `createMatrixClient()` auf, sobald ein Team mit `matrixRoomId` existiert — der dynamische Import feuert also bereits beim Rendern der Teamliste. Vorher lud die SDK erst beim Öffnen eines Teams. Netto: derselbe 829-kB-Chunk, nur **früher** und jetzt auch für Nutzer, die nie einen Chat öffnen. Das Lazy-Loading von `TeamDetail` ist dadurch weitgehend wirkungslos.
Repro: Teams-Seite laden, kein Team anklicken → Netzwerk-Tab zeigt `browser-index-*.js` (829 kB).
Priorität: P1 — Regression gegenüber dem Zustand vor dem Fix, auf Mobilfunk spürbar.
**BUG-12 (High) — App-weiter Unread-Client behandelt Token-Ablauf und Sync-Fehler überhaupt nicht**
`use-team-unread-counts.ts` reagiert nur auf `PREPARED`/`SYNCING`; es gibt keinen `ERROR`-Zweig und keinen Refresh-Pfad wie in `useTeamChat` (dort `fetchChatCredentials(..., { refresh: true })`). Der gemintete Token lebt 1 h. Danach:
(a) Die Badges frieren still ein — Nutzer bekommen neue Nachrichten nicht mehr angezeigt und merken nichts davon (genau der Zweck des Features fällt aus).
(b) matrix-js-sdk retryt `/sync` mit dem toten Token unbegrenzt weiter, weil der Client nie gestoppt wird — derselbe Fehlermodus wie BUG-5(b), jetzt aber auf einer Seite, die dauerhaft offen bleibt. Ein offener Tab erzeugt so unbegrenzt 401-Sync-Requests gegen Synapse.
Repro: Teams-Seite über eine Stunde offen lassen → Badges aktualisieren nicht mehr, Netzwerk-Tab zeigt endlose fehlschlagende `/sync`-Calls.
Priorität: P1.
**BUG-13 (Medium) — `GET /api/teams` gibt `matrixRoomId` aller Teams der Organisation an jedes Mitglied heraus**
Die Route prüft nur `requesterIsOrganizationMember`, nicht die Team-Mitgliedschaft. Bisher war die Matrix-Raum-ID durch `GET /api/chat/room` geschützt (Join auf `teamMembers` + `status='active'`). Jetzt bekommt jedes Vereinsmitglied die Raum-IDs **aller** Teams seines Vereins, auch derer, denen es nicht angehört. Kein direkter Zugriff möglich (`createRoom` nutzt `preset: "private_chat"`, also `join_rule: invite` — ein `POST /_matrix/client/v3/join/<roomId>` mit dem gemitteten Token wird abgelehnt, verifiziert am Code, nicht live), aber in Kombination mit dem nicht scopebaren Token aus BUG-2 ist es unnötige Angriffsfläche und ein Existenz-/Metadaten-Leak über Teamgrenzen hinweg.
Erwartet: `matrixRoomId` nur für Teams ausliefern, in denen der Anfragende aktives Mitglied ist (sonst `null`).
Priorität: P2.
**BUG-14 (Medium) — `leftJoin` auf `chat_rooms` kann Teams doppelt zurückgeben**
`chat_rooms.teamId` hat keinen Unique-Constraint (`backend/src/db/schema.ts:249`), nur `matrixRoomId` ist unique. Existieren für ein Team zwei Räume (z.B. durch einen fehlgeschlagenen/wiederholten `create_team_room`-Sync-Job), liefert der `leftJoin` das Team zweimal. Folgen: doppelte Kacheln in `TeamsPage`, doppelte React-Keys (`key={team.id}`), und `roomIdToTeamId` in `use-team-unread-counts.ts` mappt beide Räume auf dieselbe Team-ID, wobei der zuletzt einsortierte den Zähler überschreibt statt ihn zu addieren. Zusätzlich filtert der Join nicht auf `scope='team'`.
Erwartet: Join auf `scope='team'` einschränken und Eindeutigkeit erzwingen (Unique-Index auf `chat_rooms.team_id` bei `scope='team'`).
Priorität: P2.
**BUG-15 (Medium) — Zwei parallele Matrix-Clients mit demselben Token verschärfen das Rate-Limit-Risiko**
Ist ein Team geöffnet, laufen `useTeamUnreadCounts` und `useTeamChat` gleichzeitig, beide mit eigenem `/sync` auf denselben Nutzer-Token (im Code als bewusster Tradeoff kommentiert). Jeder ruft beim Start `POST /api/chat/token` (Limit 10/min). Dazu kommt der 15-s-Poll aus BUG-5 (4 Token-Calls/min) und ein Teardown+Neuaufbau des Unread-Clients bei jeder Änderung der `roomIds` (z.B. nach `refresh()` im Anschluss an „Team anlegen"). Ein Nutzer, der zügig durch mehrere Teams klickt, während Provisioning noch läuft, kann das 10/min-Limit erreichen → 429 → Chat zeigt einen generischen Verbindungsfehler statt „bitte kurz warten".
Priorität: P2.
**BUG-16 (Low) — `refreshCounts` prüft `cancelled` nicht**
`use-team-unread-counts.ts:59-67`: Die an `Room.timeline`/`Room.receipt` gehängte `refreshCounts` ruft `setCounts` ohne `cancelled`-Guard. Beim Unmount werden zwar `removeAllListeners()`/`stopClient()` aufgerufen, ein bereits in Flight befindlicher Callback kann aber noch ein `setState` auf einer unmounteten Komponente auslösen. Zusätzlich: schlägt `startClient()` nach dem Registrieren der Listener fehl, wird der Client im `catch` nicht gestoppt (Leak eines syncenden Clients).
Priorität: P3.
**BUG-17 (Low) — `getUnreadNotificationCount()` ist in matrix-js-sdk deprecated**
Verwendet in `use-team-chat.ts:99` und `use-team-unread-counts.ts:64`. Aktuelle SDK-Versionen empfehlen `getRoomUnreadNotificationCount(NotificationCountType.Total)`; die Deprecated-API kann bei einem SDK-Update wegfallen. Außerdem sind die in `use-team-unread-counts.ts` hartcodierten Event-Namen-Strings (`"Room.timeline"` usw., Kommentar erklärt das Warum nachvollziehbar) nicht typgeprüft — ein SDK-Rename bricht die Badges **still**, ohne Typecheck-Fehler.
Priorität: P3.
### Akzeptanzkriterien (Neubewertung)
| # | Kriterium | Runde 1 | Runde 2 |
|---|-----------|---------|---------|
| 1 | Chat-Tab in Team-Detailansicht | PASS | PASS |
| 2 | Backend liefert Token, kein Passwort/Admin-Token im Frontend | PASS | PASS |
| 3 | `/sync` via matrix-js-sdk, kein eigener WebSocket-Server | PASS | PASS |
| 4 | Alle Raum-Mitglieder dürfen senden | PASS | PASS |
| 5 | Volle Historie für neue Mitglieder | PASS m. Vorbehalt | PASS m. Vorbehalt (BUG-6 unverändert offen) |
| 6 | Ungelesen-Zähler pro Raum + Read Receipts | **FAIL** | **PASS** — Badge auf jeder Kachel; Einschränkung: friert nach 1 h ein (BUG-12) |
| 7 | Fehlendes `matrixUserId` → Hinweis + Polling | PASS | PASS |
| 8 | Kein Medien-Upload, kein Push | PASS | PASS |
**Summe: 7 PASS, 1 PASS mit Vorbehalt, 0 FAIL** (vorher 6/1/1).
### Edge Cases (Neubewertung)
| Edge Case | Runde 1 | Runde 2 |
|-----------|---------|---------|
| `matrixUserId = null` | PASS | PASS |
| `create_team_room` nicht gelaufen | PASS | PASS |
| Synapse nicht erreichbar, kein Flut-Retry | FAIL | **FAIL** — unverändert (BUG-5), zusätzlich neuer Dauer-Retry-Pfad (BUG-12b) |
| Token abgelaufen beim Senden → 1 Retry | PASS | PASS (nur im Detail-Chat; Unread-Client hat keinen Refresh, BUG-12a) |
| Rauswurf bei offenem Chat | FAIL | **PASS** |
| Lange Historie → Pagination | PASS | PASS |
### Security-Audit (Delta)
| Prüfung | Ergebnis |
|---------|----------|
| Auth-Bypass / IDOR auf Chat-Routen | PASS — unverändert |
| **Neu:** Metadaten-Leak über `GET /api/teams` | **FAIL** — BUG-13, Raum-IDs fremder Teams |
| **Neu:** Verwertbarkeit der geleakten Raum-ID | MITIGIERT — `preset: private_chat` ⇒ invite-only, Join wird abgelehnt (statisch verifiziert) |
| Tenant-Isolation des Tokens | TEILGEMILDERT — `user_directory.enabled: false` in der Sample-Config, live nicht verifizierbar (502) |
| Transportsicherheit Browser ↔ Synapse | **FAIL** — BUG-1 unverändert; `matrix.perlbach24.de` liefert derzeit 502 |
| Security-Header / CORS | **FAIL** — BUG-8 unverändert |
| XSS / SQL-Injection / Input-Validierung | PASS — unverändert, keine neuen ungeprüften Eingaben |
| Token in Logs/DB/Response | PASS — unverändert |
### Regressionstest
| Feature | Ergebnis |
|---------|----------|
| PROJ-1 Auth | PASS |
| PROJ-3 Mitgliederverwaltung | PASS |
| PROJ-5 Mannschaftsverwaltung | **RISIKO** — `GET /api/teams` hat ein neues Feld **und** durch den `leftJoin` eine geänderte Zeilenkardinalität (BUG-14). Jeder weitere Konsument der Route ist betroffen. Zusätzlich startet allein das Öffnen der Teams-Liste jetzt einen Matrix-`/sync` (vorher erst beim Öffnen eines Teams). |
| PROJ-8 Audit Log | PASS — zusätzlich zu beachten: der Unread-Client mintet beim Laden der Teams-Seite einen Token, was ggf. mehr `chat.token_issued`-Einträge erzeugt (Cache dämpft das) |
| PROJ-9 Synapse-Integration | **RISIKO unverändert** — `initial_state` + `preset: private_chat` weiterhin nicht gegen eine laufende Synapse verifiziert (502) |
| PROJ-10 Deployment | **RISIKO** — BUG-1 unverändert; Synapse-Endpoint aktuell 502 |
### Fazit Runde 2
**Production-ready: NEIN.**
- Fortschritt ist real: 2 von 3 High-Bugs sauber behoben, ein Akzeptanzkriterium von FAIL auf PASS, alle 8 Kriterien jetzt mindestens bestanden.
- Blocker unverändert: **BUG-1 (Critical)** ist nicht committet und nicht verifizierbar; Synapse ist von außen nicht erreichbar (502), damit ist das Feature derzeit ohnehin nicht betreibbar.
- Der BUG-4-Fix hat **zwei neue P1-Befunde** eingeführt (BUG-11 Bundle-Regression, BUG-12 kein Token-Refresh/Fehlerbehandlung im App-weiten Client) sowie drei P2 (BUG-13/14/15).
**Empfohlene Fix-Reihenfolge:** BUG-1 (committen + verifizieren) → Synapse-TLS-Erreichbarkeit → BUG-12 → BUG-11 → BUG-13/14 → BUG-2 (Architekturentscheidung) → BUG-5/15 → BUG-6/8 → Rest.