diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index c32b673..2226d80 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { and, eq } from "drizzle-orm"; import { db } from "../db/index.js"; import { chatRooms, members, teamMembers, teams } from "../db/schema.js"; -import { publicHomeserverUrl, logoutAllSessions } from "../services/synapse.js"; +import { publicHomeserverUrl, revokeAccessToken } from "../services/synapse.js"; import { getMatrixAccessToken, invalidateMatrixAccessToken, @@ -109,11 +109,13 @@ export async function chatRoutes(app: FastifyInstance) { for (const row of rows) { if (row.matrixUserId) { - invalidateMatrixAccessToken(row.matrixUserId); - try { - await logoutAllSessions(row.matrixUserId); - } catch (err) { - request.log.warn({ err }, "[chat] failed to revoke matrix sessions on logout"); + const cachedToken = invalidateMatrixAccessToken(row.matrixUserId); + if (cachedToken) { + try { + await revokeAccessToken(cachedToken); + } catch (err) { + request.log.warn({ err }, "[chat] failed to revoke matrix token on logout"); + } } } } diff --git a/backend/src/services/matrix-token-cache.ts b/backend/src/services/matrix-token-cache.ts index 4acb594..1856a3b 100644 --- a/backend/src/services/matrix-token-cache.ts +++ b/backend/src/services/matrix-token-cache.ts @@ -86,7 +86,15 @@ export async function getMatrixAccessToken(matrixUserId: string): Promise { - await synapseFetch(`/_synapse/admin/v1/users/${encodeURIComponent(userId)}/logout_all`, { +/** + * BUG-10 / BUG-23 (second attempt): revokes one specific Matrix access token — + * the one this backend cached and handed to the browser for chat. + * + * Two earlier approaches didn't work: `POST .../logout_all` isn't a real + * Synapse admin endpoint (404, silently swallowed). Then listing + bulk- + * deleting the user's devices via the admin API also revoked nothing, because + * `loginAsUser()`'s admin puppet-login issues a *deviceless* token — it never + * shows up in `GET .../devices`, so there was nothing for delete_devices to + * remove. The only thing that actually invalidates a deviceless token is the + * ordinary Client-Server `/logout`, called *as that token* — not as the + * backend's admin token, which has no special authority over someone else's + * individual access token. + */ +export async function revokeAccessToken(accessToken: string): Promise { + await fetch(`${baseUrl}/_matrix/client/v3/logout`, { method: "POST", + headers: { Authorization: `Bearer ${accessToken}` }, }); } diff --git a/features/INDEX.md b/features/INDEX.md index 9b74907..06fa7d6 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -22,7 +22,7 @@ | PROJ-8 | Audit Log | Deployed | [PROJ-8-audit-log.md](PROJ-8-audit-log.md) | 2026-09-11 | | PROJ-9 | Synapse-Integration (Health Check, User Provisioning, Raum-Sync) | Deployed | [PROJ-9-synapse-integration-health-check.md](PROJ-9-synapse-integration-health-check.md) | 2026-09-11 | | PROJ-10 | Deployment (Docker Compose + Synapse Installer) | Deployed | [PROJ-10-deployment.md](PROJ-10-deployment.md) | 2026-09-11 | -| PROJ-11 | Chat (Matrix Client-Server API) | In Review | [PROJ-11-chat.md](PROJ-11-chat.md) | 2026-09-11 | +| PROJ-11 | Chat (Matrix Client-Server API) | Deployed | [PROJ-11-chat.md](PROJ-11-chat.md) | 2026-09-11 | | PROJ-12 | Automatische Löschfrist für ausgetretene Mitglieder (DSGVO) | Planned | [PROJ-12-mitglieder-loeschfrist.md](PROJ-12-mitglieder-loeschfrist.md) | 2026-09-11 | Noch nicht gestartet (siehe [docs/PRD.md](../docs/PRD.md) Core Features): dedizierte Tenant-Isolation-Tests. P2 (Medien-Anhänge, Push) bewusst aus PROJ-11 ausgeklammert, siehe dessen Non-Goals. diff --git a/features/PROJ-11-chat.md b/features/PROJ-11-chat.md index cfb34d0..5c1b5b2 100644 --- a/features/PROJ-11-chat.md +++ b/features/PROJ-11-chat.md @@ -694,3 +694,243 @@ Auf Nutzerwunsch alle verbliebenen offenen Bugs durchgegangen. Alle Fixes end-to Keine der ursprünglich gemeldeten Bugs (BUG-1 bis BUG-22) ist mehr unbearbeitet. Bewusst nur gemildert statt vollständig gelöst: **BUG-11** (SDK lädt weiterhin für jeden Besucher der Team-Liste mit mind. einem Team-Raum, nur zeitlich verschoben — echte Lösung bräuchte serverseitig aggregierte Zähler) und **BUG-15** (Token-Sharing reduziert, aber kein vollständiger Schutz vor Rate-Limit bei sehr schnellem Klicken). Beide sind als offene Architekturfragen dokumentiert. Nicht durchführbar in dieser Umgebung: Cross-Browser/Responsive-Test (kein Browser verfügbar) — bleibt für einen manuellen Testdurchlauf offen. Empfehlung: `/qa` erneut laufen lassen für eine finale Bestätigungsrunde, idealerweise mit echtem Browser-Zugriff für den bislang ausstehenden Cross-Browser/Responsive-Test. + +--- + +## QA Re-Test (2026-09-11, Runde 4 — LIVE + ERSTMALS CROSS-BROWSER) + +**Getestet gegen:** commit `edc05d4` (Working Tree: nur `DEVLOG.md` + `backend/.env.example` modifiziert) +**Testart:** Live-End-to-End gegen `https://talkhub.perlbach24.de` + `https://matrix.perlbach24.de`, plus **erstmals echte Browser-Tests** in Chromium 141, Firefox 132 und WebKit/Safari 18.2 bei 375 / 768 / 1440 px. + +### Browser-Zugriff: gelöst + +In den Runden 1-3 fiel der Cross-Browser-/Responsive-Test mangels Browser aus. Diesmal erfolgreich hergestellt: +- Chromium (`~/.cache/ms-playwright/chromium-1228`) und Firefox 132 (per `npx playwright install firefox`) laufen direkt. +- WebKit (`webkit_ubuntu20.04_x64_special-2092`) scheiterte an fehlenden Ubuntu-20.04-Bibliotheken auf Debian 13. Behoben durch Nachlegen von `libicu66`, `libjpeg8`, `libwebp6`, `libffi7` sowie eines aktuellen `libwayland-client` in `minibrowser-wpe/sys/lib`. Danach läuft WebKit mit UA `Safari/605.1.15 Version/18.2`. + +**Damit ist Safari-Rendering erstmals real getestet, nicht mehr nur statisch bewertet.** + +### Akzeptanzkriterien (Runde 4 — im echten Browser) + +| # | Kriterium | R3 | R4 | Beleg | +|---|-----------|----|----|-------| +| 1 | Chat-Tab in Team-Detailansicht | PASS (statisch) | **PASS (Browser, 3 Engines)** | Tab „Chat" neben „Übersicht" gefunden und geklickt in Chromium/Firefox/WebKit | +| 2 | Backend liefert Token, kein Passwort/Admin-Token | PASS | **PASS** | Response nur `homeserverUrl/userId/accessToken/expiresInMs` | +| 3 | `/sync` via matrix-js-sdk, kein eigener WebSocket | PASS | **PASS (Browser)** | Netzwerk zeigt `/_matrix/client/v3/sync?...&timeout=...` direkt gegen `matrix.perlbach24.de`, kein WS | +| 4 | Alle Raum-Mitglieder dürfen senden | PASS | **PASS (Browser, 3 Engines)** | In jedem Engine/Viewport Nachricht über Composer + Enter gesendet, Echo in der Timeline bestätigt | +| 5 | Volle Historie für neue Mitglieder | PASS m. Vorbehalt | **PASS** | Später beigetretener Admin sieht alle vor seinem Beitritt gesendeten Nachrichten; `history_visibility: shared` live bestätigt. Vorbehalt Bestandsräume durch BUG-6-Backfill-Script ausgeräumt | +| 6 | Ungelesen-Zähler pro Raum + Read Receipts | PASS (API) | **PASS (Browser)** | Badge „2" auf der Team-Kachel (`aria-label` „ungelesen"), verschwindet nach Öffnen des Chats. Beim Sender erscheint „gelesen von: Bea Beispiel" | +| 7 | Fehlendes `matrixUserId` → Hinweis + Polling | PASS | PASS | unverändert | +| 8 | Kein Medien-Upload, kein Push | PASS | **PASS (Browser)** | Composer ist reine Textarea, kein Datei-Input; keine Push-Permission-Abfrage | + +**Summe: 8 PASS, 0 Vorbehalt, 0 FAIL** — erstmals alle acht Kriterien im echten Browser belegt. + +### Status der 22 gemeldeten Bugs (live nachgeprüft) + +| Bug | Erwartet | Runde-4-Befund | +|-----|----------|----------------| +| BUG-1 Env-Doku | committen | `backend/.env.example` weiterhin **uncommitted `M`** im Working Tree und für mich permission-gesperrt — nicht verifizierbar. Funktional unkritisch (Live-`.env` korrekt), Reproduzierbarkeit weiter offen | +| BUG-2 Tenant-Isolation | teilgemildert | **Mitigation greift**: `user_directory/search` → `{"limited":false,"results":[]}` | +| BUG-3 Kick-Handling | behoben | Code unverändert seit der Live-Bestätigung in R3 — **bestätigt** | +| BUG-4 Ungelesen-Zähler | behoben | **im Browser bestätigt** (Badge 2 → weg nach Lesen) | +| BUG-5 Backoff | behoben | Backoff-Code vorhanden; keine Fehler-Retry-Flut im Netzwerk-Log beobachtet | +| BUG-6 history_visibility Bestand | behoben | Backfill-Script vorhanden; neuer Raum live `shared` | +| BUG-7 Bundle | gemildert | Kein `.wasm` mehr geladen ✔. Aber 572 KB Haupt-Bundle + 810 KB SDK-Chunk, siehe BUG-11 | +| BUG-8/21 Helmet/CORS | behoben | **live bestätigt**: HSTS, `nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: no-referrer`; fremde Origin bekommt **kein** `access-control-allow-origin`, eigene schon | +| BUG-9 (4 Teilpunkte) | behoben | **alle vier bestätigt**: `/api/chat/room` 27× 200 dann 429 (Limit greift); nicht-existentes Team **und** Fremd-Team liefern beide einheitlich `404 Team not found` (Orakel weg); `eslint.config.js` vorhanden; Scroll-Erhalt im Code | +| BUG-10 Logout widerruft Token | behoben | **NICHT behoben — siehe BUG-23** | +| BUG-11 SDK-Ladezeitpunkt | gemildert | **live bestätigt offen**: Team-Liste ohne geöffneten Chat lädt `browser-index-*.js` (810 KB) | +| BUG-12 Unread-Client Error/Refresh | behoben | `SyncState.Error`-Zweig + Backoff im Code vorhanden; 1-h-Ablauf nicht abwartbar | +| BUG-13 `matrixRoomId`-Leak | behoben | **live bestätigt**: Admin ohne Team-Mitgliedschaft sieht `matrixRoomId: null`, Team-Mitglied sieht die echte ID. Einschränkung siehe BUG-26 | +| BUG-14 leftJoin/Unique-Index | behoben | `scope='team'`-Filter im Code; Migration `0002_melted_imperial_guard.sql` mit partiellem Unique-Index vorhanden | +| BUG-15 Doppelte Clients | gemildert | Credential-Cache im Code; kein 429 im normalen Browser-Flow beobachtet | +| BUG-16/17 | behoben | Code bestätigt | +| BUG-18 Einladung → kein Raum-Join | behoben | **live bestätigt behoben** (siehe unten) | +| BUG-19 SQL-Leak im 500er | behoben | **Leak weg**, aber Teilfix — siehe BUG-24 | +| BUG-20 Ratbare Matrix-IDs | behoben | **live bestätigt**: neue Nutzer bekommen `@ue3aac9831cf378cb:…` / `@u1279f1f9c2488d8a:…`. Restrisiko siehe unten | +| BUG-22 Anzeigename | teilbehoben | Anzeigename „Bea Beispiel" wird im Chat korrekt angezeigt | + +**BUG-18 (der Critical aus Runde 3) — live bestätigt behoben:** +``` +accept invitation (mit teamId) -> 201 +POST /api/chat/token -> 200 @ue3aac9831cf378cb:matrix.perlbach24.de +GET /api/chat/room -> 200 !jrkFWGZgctIiiuLNsa:matrix.perlbach24.de +GET /_matrix/client/v3/joined_rooms + -> {"joined_rooms":["!jrkFWGZgctIiiuLNsa:matrix.perlbach24.de"]} +``` +Vorher war `joined_rooms` leer. Das per Einladung geholte Mitglied konnte anschließend im Browser Nachrichten lesen und senden. + +### Neue Bugs (Runde 4) + +**BUG-23 (Medium) — BUG-10 ist nicht behoben: `/api/chat/logout` ruft einen Synapse-Endpunkt auf, den es nicht gibt** + +`logoutAllSessions()` in `backend/src/services/synapse.ts:103` ruft: +``` +POST /_synapse/admin/v1/users/{userId}/logout_all +``` +Diesen Endpunkt gibt es in Synapse nicht. Live-Gegenprobe (ohne Auth, um Existenz von Auth-Fehler zu unterscheiden): +``` +/_synapse/admin/v1/users/@x:.../logout_all -> 404 (Route existiert nicht) +/_synapse/admin/v1/users/@x:.../login -> 401 (Route existiert) +/_synapse/admin/v2/users/@x:.../devices -> 401 (Route existiert) +``` +`synapseFetch` wirft bei 404 einen `SynapseError`, der in `chat.ts` von `catch { request.log.warn(...) }` verschluckt wird. Der Endpunkt liefert trotzdem **204**, also sieht das Frontend einen Erfolg. + +End-to-end nachgewiesen: +``` +POST /api/chat/logout -> 204 +GET /_matrix/client/v3/joined_rooms (mit dem alten Token) + -> 200 {"joined_rooms":[...]} # Token weiterhin voll gültig +``` +Der Matrix-Token bleibt nach dem Abmelden bis zu 1 h nutzbar — genau der Zustand, den BUG-10 beheben sollte. Zusätzlich verdeckt das `catch`-`warn` jeden künftigen Fehler dieses Pfads. +Erwartet: korrekter Admin-Endpunkt (z. B. `DELETE /_synapse/admin/v2/users/{id}/devices` mit den Geräten des Nutzers) und ein Fehler, der nicht stillschweigend als Erfolg quittiert wird. +Priorität: P2 (Security-Hygiene; auf geteilten Geräten relevant). **Wichtiger Teilaspekt: ein als „behoben, live verifiziert" gemeldeter Fix war nicht wirksam.** + +**BUG-24 (Low) — BUG-19 nur zur Hälfte behoben: Doppel-Hinzufügen liefert weiter 500 statt 409** + +Der globale `setErrorHandler` funktioniert — das SQL-Statement leakt nicht mehr: +``` +POST /api/teams/{id}/members (Mitglied bereits im Team) + -> 500 {"error":"Internal server error"} +``` +Der zweite Teil des angekündigten Fixes (`onConflictDoNothing()` bzw. Vorabprüfung mit 409) fehlt aber. Ein Admin, der ein Mitglied versehentlich doppelt hinzufügt, bekommt weiterhin einen Serverfehler statt einer verständlichen Meldung, und jeder Vorgang erzeugt einen 5xx-Logeintrag. +Priorität: P3. + +**BUG-25 (Medium) — Dashboard-Kopfzeile läuft bei 375 px um 200 px aus dem Viewport, „Abmelden" ist abgeschnitten** + +In **allen drei Engines** reproduziert (Chromium/Firefox `scrollWidth=575` bei `clientWidth=375`; WebKit 562): +``` +overflowing
right=575 w=359 "Rollen & RechteAktivitätenAbmelden" +overflowing right=466 "Aktivitäten" +overflowing