From c200fafd0344dd0e9e17bc7fb1600d2dd260b6d0 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 11 Sep 2026 22:28:33 +0200 Subject: [PATCH] docs(PROJ-11): TLS infra done, end-to-end verified over public domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67 --- features/PROJ-11-chat.md | 123 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/features/PROJ-11-chat.md b/features/PROJ-11-chat.md index 474d489..da0d025 100644 --- a/features/PROJ-11-chat.md +++ b/features/PROJ-11-chat.md @@ -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/` 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.