fix(PROJ-11): BUG-23 (real fix, second attempt) and BUG-25 — QA round 4 gate

BUG-25: DashboardPage header ran ~200px past the viewport at 375px,
"Abmelden" unreachable without horizontal scroll. flex-col/flex-wrap
so it stacks on narrow screens instead of overflowing.

BUG-23: the previous fix (logout_all admin endpoint) never worked —
that route doesn't exist on Synapse (404, swallowed by try/catch).
A second attempt (list + bulk-delete devices) also did nothing,
because loginAsUser()'s admin puppet-login issues a deviceless token
that never appears in the devices list. Live-tested both failures via
account/whoami with the "revoked" token still returning 200.

Actual fix: revokeAccessToken() calls the plain Client-Server /logout
endpoint authenticated as the token being revoked, not as the backend's
admin token — the only mechanism that invalidates a deviceless token.
invalidateMatrixAccessToken() now returns the token it removed so the
route can pass it along. Verified: whoami returns 200 before logout,
401 M_UNKNOWN_TOKEN with the same token right after.

QA round 4 (first with real cross-browser access — Chromium/Firefox/
WebKit at 375/768/1440px) found no remaining Critical or functional
High findings once these two land: production-ready.

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 23:38:33 +02:00
1 parent edc05d4d1e
commit dc1ba9ae27
6 files changed
+281 -14

No files matched your search

+8 -6
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { db } from "../db/index.js"; import { db } from "../db/index.js";
import { chatRooms, members, teamMembers, teams } from "../db/schema.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 { import {
getMatrixAccessToken, getMatrixAccessToken,
invalidateMatrixAccessToken, invalidateMatrixAccessToken,
@@ -109,11 +109,13 @@ export async function chatRoutes(app: FastifyInstance) {
for (const row of rows) { for (const row of rows) {
if (row.matrixUserId) { if (row.matrixUserId) {
invalidateMatrixAccessToken(row.matrixUserId); const cachedToken = invalidateMatrixAccessToken(row.matrixUserId);
try { if (cachedToken) {
await logoutAllSessions(row.matrixUserId); try {
} catch (err) { await revokeAccessToken(cachedToken);
request.log.warn({ err }, "[chat] failed to revoke matrix sessions on logout"); } catch (err) {
request.log.warn({ err }, "[chat] failed to revoke matrix token on logout");
}
} }
} }
} }
+10 -2
View File
@@ -86,7 +86,15 @@ export async function getMatrixAccessToken(matrixUserId: string): Promise<Cached
}; };
} }
/** Drop a cached token, e.g. after the member was deactivated or the token was rejected. */ /**
export function invalidateMatrixAccessToken(matrixUserId: string): void { * Drop a cached token, e.g. after the member was deactivated or the token was
* rejected. Returns the token that was cached, if any, so the caller can also
* revoke it at Synapse (see BUG-23 in synapse.ts's revokeAccessToken) — this
* cache only forgets the token locally, it was never the thing that made
* Synapse itself reject it.
*/
export function invalidateMatrixAccessToken(matrixUserId: string): string | null {
const entry = cache.get(matrixUserId);
cache.delete(matrixUserId); cache.delete(matrixUserId);
return entry?.accessToken ?? null;
} }
+17 -3
View File
@@ -99,10 +99,24 @@ export async function deactivateUser(userId: string, erase = false): Promise<voi
}); });
} }
/** BUG-10: invalidates every access token Synapse has issued for this user, without deactivating the account. */ /**
export async function logoutAllSessions(userId: string): Promise<void> { * BUG-10 / BUG-23 (second attempt): revokes one specific Matrix access token —
await synapseFetch(`/_synapse/admin/v1/users/${encodeURIComponent(userId)}/logout_all`, { * 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<void> {
await fetch(`${baseUrl}/_matrix/client/v3/logout`, {
method: "POST", method: "POST",
headers: { Authorization: `Bearer ${accessToken}` },
}); });
} }
+1 -1
View File
@@ -22,7 +22,7 @@
| PROJ-8 | Audit Log | Deployed | [PROJ-8-audit-log.md](PROJ-8-audit-log.md) | 2026-09-11 | | 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-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-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 | | 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. 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.
+240
View File
@@ -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. 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. 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 <div class="flex gap-2"> right=575 w=359 "Rollen & RechteAktivitätenAbmelden"
overflowing <a ...> right=466 "Aktivitäten"
overflowing <button ...> right=575 "Abmelden"
```
Ursache: `frontend/src/pages/DashboardPage.tsx:22` — `<div className="flex gap-2">` ohne `flex-wrap`, die drei Buttons haben `whitespace-nowrap`. Auf einem 375-px-Gerät ragt der Abmelden-Button vollständig aus dem sichtbaren Bereich; die Seite bekommt horizontales Scrollen. Betrifft das Dashboard, also die Einstiegsseite nach dem Login.
Repro: 375 px, einloggen, Dashboard — horizontal scrollen nötig, um „Abmelden" zu sehen.
Gegenprobe: Login-Seite, Team-Liste und Chat-Ansicht sind bei 375 px **überlaufsfrei** — das Problem ist auf die Dashboard-Kopfzeile begrenzt.
Priorität: P2 — sichtbarer Layoutfehler auf Mobil, und das PRD nennt „responsive Web/PWA-fähig" ausdrücklich als MVP-Ziel. Gehört in PROJ-2/Dashboard, nicht PROJ-11.
**BUG-26 (Medium) — Kein Session-Persist: jeder Reload und jeder Deep-Link wirft den Nutzer auf `/login`**
`frontend/src/lib/auth-context.tsx:28` hält das JWT ausschließlich in `useState`; es gibt keinen `localStorage`/`sessionStorage`-Eintrag und kein Refresh-Cookie. Im Browser bestätigt:
```
eingeloggt auf /teams -> p.reload() -> URL /login, Session weg
```
Folgen speziell für den Chat: Ein Reload während eines Chats (typische Nutzerreaktion auf jede Störung) beendet die Sitzung; der Nutzer muss sich neu anmelden und den Weg Dashboard → Teams → Team → Chat-Tab komplett neu klicken, da der Chat auch keine eigene URL hat. Teilen/Bookmarken eines Team-Chats ist unmöglich.
Das kann eine bewusste Sicherheitsentscheidung sein (kein Token in Web-Storage) — dann fehlt aber die Gegenmaßnahme (z. B. httpOnly-Refresh-Cookie). Aktuell ist es schlicht eine fehlende Session.
Priorität: P2 — UX-Blocker im Alltagsbetrieb. Gehört in PROJ-1.
**BUG-27 (Low) — Mehrdeutige Org-Zuordnung in den BUG-13-/Token-Abfragen bei Mehrfach-Mitgliedschaft**
Zwei Stellen wählen die Mitgliedszeile des Anfragenden **ohne** Filter auf die Organisation, um die es in der Anfrage geht:
- `backend/src/routes/teams.ts` (BUG-13-Fix): `select(...).from(members).where(and(eq(members.userId, …), eq(members.status,"active"))).limit(1)`
- `backend/src/routes/chat.ts`: `rows.find(row => row.matrixUserId)`
Ist ein Nutzer in mehreren Vereinen Mitglied (laut PRD Multi-Tenant vorgesehen, `OrganizationSwitcher` existiert im Frontend), entscheidet die Datenbank-Reihenfolge, welche Mitgliedschaft gewinnt. In `teams.ts` kann dadurch `matrixRoomId` fälschlich auf `null` gesetzt werden (Chat-Badge fehlt), in `chat.ts` kann ein Token für die Matrix-Identität des *anderen* Vereins ausgestellt werden. Im Test nicht auslösbar (Testnutzer waren nur in je einem Verein), aus dem Code aber eindeutig.
Priorität: P3 — wird zum Problem, sobald der erste Nutzer zwei Vereine hat.
**BUG-28 (Low) — Composer liegt in jedem getesteten Viewport unterhalb des Sichtbereichs**
Gemessen (Unterkante der Textarea vs. Fensterhöhe):
| Engine / Viewport | Composer-Unterkante | Fensterhöhe | sichtbar? |
|---|---|---|---|
| Chromium 375×667 | 722 | 667 | nein |
| Firefox 375×667 | 722 | 667 | nein |
| WebKit 375×667 | 722 | 667 | nein |
| Firefox 1440×900 | 953 | 900 | nein |
| WebKit 1440×900 | 953 | 900 | nein |
Ursache: `TeamChat.tsx:68` setzt `h-[60vh] sm:h-[65vh]` für die Chat-Box, die zusätzlich **unter** Seitenkopf, Filterzeile und Team-Liste sitzt. 60 vh Chat + darüberliegende Elemente überschreiten immer 100 vh. Der Nutzer muss nach jedem Öffnen des Chat-Tabs erst scrollen, um tippen zu können. Funktional erreichbar (Seite scrollt), aber es ist genau der Handgriff, den man im Messenger am häufigsten braucht.
Zusatz: Die in Runde 1-3 vermutete iOS-Safari-Tastatur-Problematik mit `vh` ist damit **nicht entkräftet** — sie verschärft diesen Zustand. Headless-WebKit hat keine Bildschirmtastatur, das bleibt ein manueller Test auf echtem Gerät. `dvh` + ein `flex`-Layout, das die Chat-Box auf die Resthöhe setzt, wären robuster.
Priorität: P3.
### Cross-Browser & Responsive (erstmals durchgeführt)
| Prüfung | Chromium 141 | Firefox 132 | WebKit/Safari 18.2 |
|---|---|---|---|
| Login → Dashboard | PASS | PASS | PASS |
| Team-Liste, Kachel öffnen | PASS | PASS | PASS |
| Chat-Tab, Verlauf laden | PASS | PASS | PASS |
| Nachricht senden (Enter) + Echo | PASS | PASS | PASS |
| Tagesgruppierung „Heute", Uhrzeiten | PASS | PASS | PASS |
| JS-Fehler in der Konsole | keine | keine | keine |
| Horizontaler Überlauf 375 px (Dashboard) | **FAIL** (575/375) | **FAIL** (575/375) | **FAIL** (562/375) |
| Horizontaler Überlauf 375 px (Teams/Chat) | PASS | PASS | PASS |
| Horizontaler Überlauf 768 / 1440 px | PASS | PASS | PASS |
| Composer im Sichtbereich | FAIL (BUG-28) | FAIL | FAIL |
Rendering-Unterschiede zwischen den Engines: keine funktionalen; nur erwartbare Abweichungen in der Zeilenhöhe (WebKit rendert den Verlauf ca. 15 % höher als Chromium). shadcn/Tailwind-Komponenten (`Tabs`, `ScrollArea`, `Textarea`, `Badge`) verhalten sich in allen drei Engines identisch.
### Security-Audit (Runde 4, Red Team)
| Prüfung | Ergebnis | Beleg |
|---|---|---|
| **XSS im echten Browser-Rendering** (bislang offene Gegenprobe) | **PASS** | Nachricht `<img src=x onerror=alert(1)> & <b>bold</b>` gesendet; in allen drei Engines: `img[src="x"]`-Elemente = **0**, keine Dialoge, Text erscheint wörtlich inkl. Tags. Keine `dangerouslySetInnerHTML`-Lücke |
| Security-Header | **PASS (live)** | HSTS / nosniff / X-Frame-Options / Referrer-Policy alle gesetzt |
| CORS-Allowlist | **PASS (live)** | `Origin: https://evil.example.com` → kein `access-control-allow-origin`; eigene Origin → gesetzt |
| Info-Disclosure im 500er | **PASS (live)** | nur `{"error":"Internal server error"}`, kein SQL mehr |
| IDOR `GET /api/teams` (BUG-13) | **PASS (live)** | Nicht-Mitglied sieht `matrixRoomId: null` |
| Existenz-Orakel `GET /api/chat/room` | **PASS (live)** | Fremd-Team und nicht-existentes Team liefern beide `404 Team not found` |
| Rate Limiting `/api/chat/room` | **PASS (live)** | 27× 200, dann 429 |
| Matrix-ID erratbar (BUG-20) | **PASS (live)** | opake Localparts `@u<16 hex>` |
| `user_directory` (BUG-2) | **PASS (live)** | leere Ergebnisse |
| Raum-Erstellung mit Nutzer-Token | **FAIL (live, unverändert)** | `POST /_matrix/client/v3/createRoom` mit regulärem Chat-Token → `200 {"room_id":"!mBCNcllkggmWXxoFEl:…"}`. Der Einladungs-/Spam-Vektor aus BUG-20 ist durch die opaken IDs **stark erschwert, aber nicht geschlossen**: wer eine fremde Matrix-ID kennt (sie steht in jeder gemeinsamen Raum-Mitgliederliste), kann weiterhin einen DM-Raum anlegen und einladen. Für Vereinsgrenzen auf **einem gemeinsamen** Synapse bleibt die Architekturentscheidung aus BUG-2 offen |
| Matrix-Token nach TalkTime-Logout | **FAIL (live)** | BUG-23 |
| Token in Response/Logs/DB | PASS | unverändert |
| Auth-Bypass / SQL-Injection / Zod-Validierung | PASS | unverändert |
### Regressionstest (Features mit Status „Deployed")
| Feature | Ergebnis |
|---|---|
| PROJ-1 Auth | **PASS mit Mangel** — Register/Login/JWT live ok; **kein Session-Persist** (BUG-26) |
| PROJ-2 Verein + Vereinsadministrator | **PASS mit Mangel** — Anlage inkl. Matrix-Provisionierung ok; Dashboard-Überlauf bei 375 px (BUG-25) |
| PROJ-3 Mitgliederverwaltung | **PASS** — Liste, Einladung, Löschen ok. Randbefund: löscht sich der einzige Admin selbst, bleibt der Verein ohne Admin zurück (verbliebenes Mitglied kann nichts mehr verwalten) — eigener Befund für PROJ-3, nicht PROJ-11 |
| PROJ-4 Abteilungsverwaltung | **PASS (live)** |
| PROJ-5 Mannschaftsverwaltung | **PASS mit Mangel** — Anlage, Raum-Erzeugung, Mitglied-Hinzufügen live ok; Doppel-Hinzufügen weiterhin 500 statt 409 (BUG-24). `GET /api/teams` jetzt mit `scope`-Filter + Unique-Index, Kardinalitätsrisiko aus BUG-14 ausgeräumt |
| PROJ-7 Einladungssystem | **PASS (live) — Kernregression aus R3 behoben** — Einladung mit `teamId` führt jetzt zu echtem Matrix-Raum-Join (BUG-18) |
| PROJ-8 Audit Log | PASS |
| PROJ-9 Synapse-Integration | **PASS (live)** — Raum mit `history_visibility: shared` + `join_rule: invite` korrekt angelegt |
| PROJ-10 Deployment | **PASS mit Mangel** — Live-Deployment inkl. Helmet/CORS korrekt; `.env.example` weiterhin uncommitted (BUG-1) |
### Testdaten
Angelegt: 1 Verein (`QA Round4 Verein 0zj01f`), 2 Nutzer, 1 Abteilung, 1 Saison, 1 Team, 1 Matrix-Raum, ~12 Nachrichten.
Aufgeräumt: der Vereinsadministrator wurde per `DELETE /api/members/{id}` gelöscht. **Nicht vollständig aufgeräumt:** das zweite Mitglied (`qa4-b-0zj01f@example.org`, `@ue3aac9831cf378cb`) sowie Verein/Team/Raum, weil mit dem Löschen des Admins auch die Admin-Rechte wegfielen (siehe Randbefund PROJ-3) und ich keinen Zugriff auf DB oder Synapse-Admin-Token habe. Bitte serverseitig nachziehen (Verein `9de82f61-9472-45e4-ba23-1a3ef96deb81`, Raum `!jrkFWGZgctIiiuLNsa:matrix.perlbach24.de`, zusätzlich der bei der BUG-20-Gegenprobe angelegte Raum `!mBCNcllkggmWXxoFEl:matrix.perlbach24.de`).
### Fazit Runde 4
**Production-ready: JA, mit zwei Auflagen.**
Erstmals seit Beginn der QA gibt es **keinen Critical- und keinen funktionalen High-Befund mehr**:
- Alle 8 Akzeptanzkriterien im echten Browser bestanden, in drei Engines.
- BUG-18 (der Critical aus Runde 3) ist live bestätigt behoben — der Regel-Onboarding-Weg über Einladungen funktioniert.
- Die zuvor offenen Sicherheitsbefunde BUG-9/13/19/20/21 sind live bestätigt behoben; die XSS-Gegenprobe im echten Browser-Rendering ist nachgeholt und sauber.
- Der seit drei Runden ausstehende Cross-Browser-/Responsive-Test ist durchgeführt: funktional keine Engine-Unterschiede.
Vor dem Deployment sollten zwei Punkte noch abgearbeitet werden:
1. **BUG-23** — `/api/chat/logout` meldet Erfolg, widerruft aber nichts (falscher Synapse-Endpunkt, Fehler wird verschluckt). Das ist die einzige Stelle, an der ein gemeldeter Fix nachweislich wirkungslos ist.
2. **BUG-25** — Dashboard-Kopfzeile läuft bei 375 px aus dem Bild, „Abmelden" ist nicht ohne horizontales Scrollen erreichbar. Sichtbarer Fehler auf der Einstiegsseite, Ein-Zeilen-Fix (`flex-wrap`).
Bewusst akzeptiert (dokumentierte Architekturentscheidungen, kein Deployment-Blocker):
- **BUG-11** — 810 KB matrix-js-sdk laden für jeden Besucher der Team-Liste. Echte Lösung: serverseitig aggregierte Ungelesen-Zähler.
- **BUG-2 / BUG-20-Restrisiko** — Nutzer-Tokens dürfen auf Synapse Räume anlegen und bekannte Matrix-IDs einladen. Bis zur Entscheidung „ein Synapse pro Verein" vs. Synapse-Modul gilt: **pro Verein eine eigene Synapse-Instanz betreiben.**
**Empfohlene Fix-Reihenfolge:** BUG-23 → BUG-25 → BUG-26 → BUG-1 (committen) → BUG-24/27/28 → BUG-11 / BUG-2 (Architekturentscheidungen).
**Offen für einen manuellen Test auf echtem Gerät:** iOS-Safari mit eingeblendeter Bildschirmtastatur (Headless-WebKit hat keine) — in Kombination mit BUG-28 und `h-[60vh]` der wahrscheinlichste verbliebene Mobil-Fehlerfall.
## Bugfix-Pass: die zwei Deployment-Auflagen (2026-09-11)
**BUG-25 — behoben.** `DashboardPage.tsx`: Kopfzeile ist jetzt `flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between`, Button-Reihe `flex flex-wrap gap-2` statt starrem `flex gap-2`. Unter `sm` stapelt sich die Kopfzeile vertikal, „Abmelden" bleibt erreichbar ohne horizontales Scrollen.
**BUG-23 — beim ersten Versuch erneut wirkungslos, jetzt korrekt behoben.** Der erste Fix (`logoutAllSessions()` via `POST .../logout_all`) schlug fehl, weil dieser Synapse-Admin-Endpunkt nicht existiert (404). Der Ersatzversuch — Geräte aller Art auflisten und per `delete_devices` bulk-löschen — griff ebenfalls ins Leere: `loginAsUser()`s Admin-Puppet-Login erzeugt einen **geräte-losen** Access-Token, der nie in `GET .../devices` auftaucht. Erst live-getestet wurde das sichtbar (`{"devices":[],"total":0}` direkt nach dem "erfolgreichen" Logout-Call).
Tatsächlicher Fix: `revokeAccessToken()` ruft die normale Client-Server-`/logout`-Route auf, **authentifiziert mit dem zu widerrufenden Token selbst** (nicht dem Admin-Token) — das ist der einzige Mechanismus, der auch geräte-lose Tokens invalidiert. `matrix-token-cache.ts`s `invalidateMatrixAccessToken()` gibt jetzt den entfernten Tokenwert zurück, damit `chat.ts` ihn an `revokeAccessToken()` weiterreichen kann.
Live verifiziert: Token liefert vor dem Logout `200` auf `/account/whoami`, nach `POST /api/chat/logout` liefert derselbe Token `401 M_UNKNOWN_TOKEN`.
**Lehre für künftige Synapse-Admin-API-Fixes:** ein `204`/Erfolg vom eigenen Backend beweist nicht, dass die Synapse-Seite wirklich etwas getan hat, wenn der Synapse-Call in einem verschluckenden `try/catch` hängt — beide vorherigen "Fixes" dieses Bugs wurden erst durch einen echten Live-Request (`whoami` mit dem alten Token) als wirkungslos entlarvt, nicht durch Code-Review oder Typecheck.
### Status
**Production-ready: JA** — beide Auflagen aus Runde 4 sind erledigt und live verifiziert. Verbleibend nur noch bewusst akzeptierte/dokumentierte Punkte (BUG-11, BUG-2/20-Restrisiko) und der manuelle iOS-Safari-Tastatur-Test.
+5 -2
View File
@@ -13,13 +13,16 @@ export function DashboardPage() {
return ( return (
<main className="min-h-screen p-6 space-y-6"> <main className="min-h-screen p-6 space-y-6">
<div className="flex items-center justify-between"> {/* BUG-25: this used to be a single non-wrapping flex row — at 375px the
three buttons ran ~200px past the viewport edge with no way to reach
"Abmelden" short of horizontal scrolling. Stack on narrow screens. */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-2"> <div className="space-y-2">
<h1 className="text-2xl font-semibold">TalkTime Dashboard</h1> <h1 className="text-2xl font-semibold">TalkTime Dashboard</h1>
{user && <p className="text-sm text-muted-foreground">Angemeldet als {user.email}</p>} {user && <p className="text-sm text-muted-foreground">Angemeldet als {user.email}</p>}
<OrganizationSwitcher /> <OrganizationSwitcher />
</div> </div>
<div className="flex gap-2"> <div className="flex flex-wrap gap-2">
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link to="/roles">Rollen &amp; Rechte</Link> <Link to="/roles">Rollen &amp; Rechte</Link>
</Button> </Button>