Compare commits
35 Commits
beta
...
725f03abb0
| Author | SHA1 | Date | |
|---|---|---|---|
| 725f03abb0 | |||
| 15001ae80a | |||
| 1ef32c0022 | |||
| 66bf54bdff | |||
| 81ec06398f | |||
| 4477ed9101 | |||
| 825481e6fa | |||
| db4d001ef1 | |||
| af7c538e18 | |||
| 4c6efa2a7a | |||
| d3e75c36a1 | |||
| 326cc598c7 | |||
| 13b333cbec | |||
| 4d4044b888 | |||
| 5ada0e5038 | |||
| c8ff850c98 | |||
| d42beaf7ab | |||
| 343545b59e | |||
| 455742d1bf | |||
| e97a748b35 | |||
| ba97f942d6 | |||
| 88af6c016c | |||
| 78e4174658 | |||
| 925aafb2fe | |||
| fe6427bfba | |||
| a5a5c4baf5 | |||
| 7b90f20e36 | |||
| 03b84e0e0e | |||
| 7dfdbf1159 | |||
| e0ac5cb9fd | |||
| eebf8ae7dd | |||
| 566938f004 | |||
| 1630d0e389 | |||
| 99cde9a1e2 | |||
| bfe58760ee |
@@ -0,0 +1,42 @@
|
||||
# Bernds Entscheidungen – Richtungsfragen aus PROGRESS.md beantwortet
|
||||
|
||||
Diese Datei beantwortet die über mehrere Sessions in PROGRESS.md gesammelten
|
||||
„Fragen an Bernd". Sie ist ab jetzt **maßgeblich** – die betroffenen Punkte in
|
||||
ROADMAP.md/PROGRESS.md gelten als entschieden.
|
||||
Zuletzt aktualisiert: **2026-07-05** (Bernd + Claude am PC).
|
||||
|
||||
## 1. SQLCipher (lokale DB + Access-Token verschlüsseln) → VOR M2 priorisieren
|
||||
- **Entscheidung:** Ja. Die Verschlüsselung der lokalen `pyramid.sqlite` hat Vorrang
|
||||
vor dem M2-Refactoring. Betrifft den M1-Punkt „Sichere Speicherung von Access-Token
|
||||
& Krypto-DB".
|
||||
- Umsetzung nach `docs/SQLCIPHER_MIGRATION.md`. **Wichtig:** Die Migration bestehender
|
||||
Klartext-DBs (Utas Gerät!) ist der riskante Teil und braucht einen echten
|
||||
PC-/Gerätetest. Auf dem Pi (kein GUI) nur so weit vorbereiten, wie es OHNE GUI sicher
|
||||
testbar ist; alles Verhaltensrelevante klar als UNGETESTET markieren und erst nach
|
||||
`docs/PC_TESTPLAN.md` als erledigt abhaken. Grundsatz „Kein Datenverlust" – im Zweifel
|
||||
für den PC-Termin liegen lassen.
|
||||
|
||||
## 2. M2 – erstes Umbau-Modul → Call-Pilot zuerst
|
||||
- **Entscheidung:** Wenn M2 startet (nach SQLCipher), zuerst die Call-Schicht entwirren
|
||||
(Call-Pilot / gemeinsame Fassade über `voip_manager.dart` + `livekit_call_manager.dart`),
|
||||
wie in `docs/M2_MODULE_SCHNITT.md` vorgeschlagen. **Nicht** zuerst `settings_modal.dart`
|
||||
aufsplitten.
|
||||
- **Wichtig:** heiliger Call-Pfad → braucht zwingend echten Call-Test auf einem Gerät.
|
||||
Auf dem Pi nicht blind umbauen; Vorbereitung/Analyse ok, Umsetzung + Test am PC.
|
||||
|
||||
## 3. Dashboard-Fernzugriff (stats.html) → Heimnetz-only lassen
|
||||
- **Entscheidung:** KEIN Fernzugriff. Das Interims-Gate (nur Heimnetz/localhost) bleibt so.
|
||||
Die offene A/B-Frage (Cloudflare Access vs. DASH_TOKEN) aus
|
||||
`docs/DASHBOARD_AUTH_HARDENING.md` ist damit **geschlossen** – nicht weiter daran
|
||||
arbeiten. Unterwegs per WireGuard erreichbar.
|
||||
|
||||
## 4. Git-History / Admin-Token → vorerst NICHTS tun
|
||||
- **Entscheidung:** Kein Git-History-Rewrite und aktuell keine Token-Rotation durch den
|
||||
Autopilot. Der Admin-Token ist bereits aus dem App-Code heraus (eigener `DIAG_TOKEN`).
|
||||
Eine spätere Rotation bleibt Bernds manuelle Sache – nicht eigenmächtig anstoßen.
|
||||
|
||||
---
|
||||
|
||||
**Kurzfassung fürs Arbeiten:** SQLCipher vor M2, dann Call-Pilot als erstes M2-Modul.
|
||||
Dashboard bleibt Heimnetz-only. History unangetastet. Riskante/GUI-relevante Teile
|
||||
warten auf den PC-Termin (`docs/PC_TESTPLAN.md`); auf dem Pi nur sicher Vorbereitbares.
|
||||
+110
@@ -1199,3 +1199,113 @@
|
||||
2026-07-03 17:51 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid_push.md
|
||||
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-03 17:52 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/MEMORY.md
|
||||
2026-07-03 18:16 [Write] /home/steggi/pyramid/test/soft_logout_guard_test.dart
|
||||
2026-07-03 18:17 [Write] /home/steggi/pyramid/test/power_levels_safety_test.dart
|
||||
2026-07-03 22:21 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-03 22:22 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-03 22:23 [Write] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-03 22:23 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||
2026-07-03 22:48 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-03 22:50 [Write] /home/steggi/pyramid/test/auth_log_test.dart
|
||||
2026-07-03 22:50 [Edit] /home/steggi/pyramid/pubspec.yaml
|
||||
2026-07-03 22:51 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-03 22:52 [Write] /home/steggi/pyramid/test/settings_prefs_test.dart
|
||||
2026-07-03 22:53 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-03 22:53 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||
2026-07-03 22:54 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
|
||||
2026-07-03 23:18 [Edit] /home/steggi/pyramid/test/settings_prefs_test.dart
|
||||
2026-07-03 23:19 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
|
||||
2026-07-03 23:20 [Edit] /home/steggi/pyramid/lib/core/settings_prefs.dart
|
||||
2026-07-03 23:21 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-03 23:21 [Edit] /home/steggi/pyramid/docs/PC_TESTPLAN.md
|
||||
2026-07-03 23:22 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 03:28 [Edit] /home/steggi/pyramid/scripts/release.ps1
|
||||
2026-07-04 03:31 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 03:32 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 03:32 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 04:01 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 04:02 [Edit] /home/steggi/pyramid/lib/widgets/settings/settings_encryption.dart
|
||||
2026-07-04 04:03 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 04:03 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 04:06 [Write] /home/steggi/pyramid/docs/LIVEKIT_TOKEN_MIGRATION.md
|
||||
2026-07-04 04:06 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 04:06 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 04:07 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-04 04:30 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 04:30 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 04:31 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 04:32 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 04:33 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 08:42 [Write] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||
2026-07-04 08:42 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 08:43 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 08:43 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 09:14 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||
2026-07-04 09:15 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 09:16 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 09:17 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 09:18 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 09:43 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 13:46 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 13:46 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 13:50 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 13:51 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 13:51 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 13:51 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-04 14:17 [Write] /home/steggi/matrix/server.py
|
||||
2026-07-04 14:19 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 14:20 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 14:20 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 14:43 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 14:44 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 14:45 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 14:46 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 14:46 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 14:47 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 18:54 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 18:56 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 18:56 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 18:56 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 18:57 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-04 19:19 [Edit] /home/steggi/matrix/server.py
|
||||
2026-07-04 19:20 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 19:21 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 19:21 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 19:22 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-04 19:44 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 19:44 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-04 19:44 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-04 19:45 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-05 14:51 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-05 14:52 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-05 14:52 [Edit] /home/steggi/pyramid/ROADMAP.md
|
||||
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||
2026-07-05 14:52 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||
2026-07-05 14:53 [Edit] /home/steggi/pyramid/docs/DASHBOARD_AUTH_HARDENING.md
|
||||
2026-07-05 14:53 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-05 14:54 [Write] /home/steggi/.claude/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand-haupt.md
|
||||
2026-07-05 14:54 [Edit] /home/steggi/.claude-pyramid/projects/-home-steggi-pyramid/memory/pyramid-arbeitsstand.md
|
||||
2026-07-05 14:54 [Edit] /home/steggi/.claude/projects/-home-steggi-pyramid/memory/MEMORY.md
|
||||
2026-07-05 15:18 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-05 15:18 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-05 18:40 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
2026-07-05 18:41 [Edit] /home/steggi/pyramid/PROGRESS.md
|
||||
|
||||
@@ -9,6 +9,9 @@ Einsatz (u. a. Uta) – Stabilität und Datensicherheit gehen vor Feature-Tempo.
|
||||
(`C:\Users\nordm\pyramid` und `C:\Users\nordm\MatrixPi\pyramid` sind veraltete
|
||||
April-Schnappschüsse – dort NIE arbeiten.)
|
||||
|
||||
> **Bernds Entscheidungen zu den offenen Richtungsfragen stehen in `ANTWORTEN_BERND.md`
|
||||
> (zuletzt 2026-07-05) – vor dem Abarbeiten von ROADMAP-Punkten lesen, sie sind maßgeblich.**
|
||||
|
||||
## Infrastruktur
|
||||
|
||||
- Homeserver: Continuwuity auf dem Pi5 (`steggi-matrix.work`)
|
||||
|
||||
+1051
File diff suppressed because it is too large
Load Diff
+76
@@ -60,6 +60,82 @@ Punkte abhaken (`[x]`), wenn erledigt UND in `PROGRESS.md` protokolliert.
|
||||
- [x] Geräte-/Sessionverwaltung in den Einstellungen: bereits vorhanden
|
||||
(`_SessionsSection` in `settings_modal.dart` – Liste, Umbenennen, Verifizieren
|
||||
per SAS/QR, Abmelden, Massen-Abmeldung mit Passwort-Reauth)
|
||||
- [x] **SICHERHEIT: Dashboard-Admin-Endpoints – ERLEDIGT: Loch geschlossen,
|
||||
Heimnetz-only ist laut Bernd der Endzustand** (Fable-5-Review (o);
|
||||
Entscheidung `ANTWORTEN_BERND.md` Nr. 3, 2026-07-05).
|
||||
`server.py` nimmt `/api/ban`, `/api/unban`, `/api/toggle-registration`,
|
||||
`/api/create-invite`, `/api/run-stats` jetzt NUR noch aus Heimnetz/localhost an
|
||||
(Socket-IP + Cloudflare-Header-Check); Fremde aus dem Internet bekommen 403.
|
||||
Headless verifiziert (öffentlich 403, Heimnetz ok, App-Endpoints unverändert),
|
||||
siehe PROGRESS.md 2026-07-04. **Bernds Dashboard läuft im Heimnetz unverändert:
|
||||
`http://192.168.178.71:8080/stats.html`.** Offen bleibt: Will Bernd die
|
||||
Admin-Buttons auch von unterwegs? Dann Weg A (Cloudflare Access, empfohlen –
|
||||
schließt auch die weiterhin öffentliche Nutzerlisten-Ansicht) oder Weg B
|
||||
(`DASH_TOKEN`), Plan + Restrisiken in `docs/DASHBOARD_AUTH_HARDENING.md`.
|
||||
Zusatz (Fable-5-Review (p), 2026-07-04): `server.py` läuft jetzt multithreaded
|
||||
mit 30-s-Socket-Timeout – ein einzelner langsamer Client kann den Server
|
||||
(und damit `/api/livekit-token`) nicht mehr blockieren; headless verifiziert.
|
||||
Zusatz (Fable-5-Review (q), 2026-07-04): Der durch (p) eingeführte
|
||||
Nebenläufigkeits-Race in den Admin-Routen (Read-Modify-Write auf `conduit.toml`,
|
||||
`send_admin`-Readback) ist mit `ADMIN_LOCK` geschlossen, das NUR die
|
||||
Admin-Mutationen serialisiert – die öffentlichen Routen bleiben nebenläufig;
|
||||
headless verifiziert (2× parallel = 2× Laufzeit, öffentlicher Pfad 11 ms).
|
||||
Zusatz (Fable-5-Review (r), 2026-07-04): drei Restbefunde gefixt –
|
||||
`registration-status`-Leser jetzt unter ADMIN_LOCK (Truncate-Write-Race),
|
||||
negative/übergroße `Content-Length` überall abgelehnt (kein Thread-Hänger,
|
||||
ban/unban 4-KB-Deckel), Einladungs-Token per `secrets` statt `random`.
|
||||
Zusatz (Fable-5-Review (s), 2026-07-04): ban/unban-Body wird jetzt VOR dem
|
||||
ADMIN_LOCK gelesen (hängender Body-Read konnte den Lock 30 s festhalten),
|
||||
und die öffentlichen Routen antworten bei internen Fehlern generisch statt
|
||||
mit Exception-Text (Info-Leak); Details ins Journal. Headless verifiziert.
|
||||
Zusatz (Fable-5-Review (t), 2026-07-04): auch der öffentliche GET-Leser
|
||||
`registration-status` antwortet bei internen Fehlern jetzt generisch
|
||||
statt mit `str(e)` (hätte den `conduit.toml`-Pfad geleakt); headless
|
||||
verifiziert inkl. 500-Beweis.
|
||||
Zusatz (Fable-5-Review (u), 2026-07-04): (t)-Fix gegengelesen – korrekt,
|
||||
kein Befund; die server.py-Härtungskette ist damit KONVERGIERT (alle
|
||||
verbliebenen `str(e)`-Antworten LAN-gegated, absichtlich).
|
||||
**Abschluss (2026-07-05, `ANTWORTEN_BERND.md` Nr. 3): KEIN Fernzugriff
|
||||
gewünscht – die A/B-Frage ist GESCHLOSSEN, das Heimnetz-Gate ist der
|
||||
Endzustand (unterwegs nutzt Bernd WireGuard). Die Restrisiken des Gates
|
||||
(öffentliche Nutzerlisten-Ansicht, theoretisches LAN-CSRF) sind bewusst
|
||||
akzeptiert und in `docs/DASHBOARD_AUTH_HARDENING.md` dokumentiert.
|
||||
Nicht weiter daran arbeiten.**
|
||||
- [x] **LiveKit-Token-Route server-seitig gebaut + verifiziert** (Fable-5-Review (n),
|
||||
PROGRESS.md 2026-07-04). `POST /api/livekit-token` läuft live in `server.py`
|
||||
(whoami-Auth, stdlib-HS256-Minting aus `livekit.yaml`), headless geprüft
|
||||
(401/400/413/200, JWT-Signatur unabhängig gegen `livekit.yaml` gültig, Identität =
|
||||
geprüfte user_id). **Offen:** Client-Umstellung (`livekit_token.dart`/
|
||||
`livekit_call_manager.dart`) + Secret-Rotation – beides PC/Gerät (heiliger
|
||||
Call-Pfad), siehe `docs/LIVEKIT_TOKEN_MIGRATION.md`.
|
||||
- [ ] **SICHERHEIT: geleakte Secrets rotieren + aus dem Client holen** (Fable-5-Review (k),
|
||||
PROGRESS.md 2026-07-04). Das Gitea-Repo ist öffentlich (`private:false`), und es lagen
|
||||
vier aktive Secrets drin – teils schon bereinigt (release.ps1, Admin-Doku), aber:
|
||||
- **Rotieren (Pflicht, sie sind bereits geleakt):** Matrix-Server-Admin-Token
|
||||
`J5lax…` (noch gültig, voller Admin!), Gitea-Release-Token, LiveKit-`apiKey`/
|
||||
`apiSecret`, Giphy-Key. Alte jeweils widerrufen.
|
||||
- **LiveKit-Token server-seitig minten:** kleiner Token-Endpoint auf dem Pi, damit
|
||||
`apiSecret` aus `lib/core/livekit_token.dart` verschwindet (App holt nur das JWT).
|
||||
**Umsetzungsplan liegt fertig in `docs/LIVEKIT_TOKEN_MIGRATION.md`** (2026-07-04,
|
||||
gegen Code + Pi-Konfig geschrieben) – braucht echten Call-Test auf einem Gerät.
|
||||
- **E2EE-Diagnose-Upload: ERLEDIGT (2026-07-04).** Der Dashboard-Server auf dem Pi
|
||||
(`/home/steggi/matrix/server.py`) benutzte für `/api/e2ee-diagnostics` denselben
|
||||
String wie den Matrix-Admin-Token – deshalb steckte er im Client. Jetzt hat das
|
||||
Diagnose-Endpoint einen eigenen, eng begrenzten Token (`DIAG_TOKEN`, hängt nur an
|
||||
den Log an, keine Admin-Rechte); `settings_encryption.dart` benutzt diesen. Der
|
||||
**Admin-Token ist damit nicht mehr im App-Code** (per `grep` bestätigt). Server +
|
||||
Client umgestellt und verifiziert (neuer Token 200, alter Admin-Token 403). Der
|
||||
Admin-Token selbst muss trotzdem noch rotiert werden (steht oben, Bernds Sache).
|
||||
- Optional: Git-History-Rewrite (Bernds Entscheidung, Repo ist auch Backup).
|
||||
- **Stand 2026-07-05 (`ANTWORTEN_BERND.md` Nr. 4): Rotation und
|
||||
History-Rewrite ruhen BEWUSST – beides bleibt Bernds manuelle Sache,
|
||||
NICHT vom Autopilot anstoßen oder nachfragen. Der Punkt bleibt nur als
|
||||
Merkposten offen; Autopilot-bearbeitbar ist hier nichts mehr.**
|
||||
- **ACHTUNG bei „Repo einfach privat schalten":** Der In-App-Updater
|
||||
(`lib/core/update_checker.dart`) holt Releases **anonym** über die öffentliche
|
||||
Gitea-API (`/api/v1/repos/steggi/pyramid/releases`). Repo privat = Utas App
|
||||
findet keine Updates mehr (und ein Fix ließe sich nicht mehr per Update
|
||||
verteilen – Henne-Ei). Erst Updater-Strategie klären, dann Sichtbarkeit ändern.
|
||||
- [ ] Härtetest dokumentieren: Login → Nachrichten → Logout → Login neu → alles noch lesbar
|
||||
(gehört zum ausstehenden PC-Praxistest der beiden Bugfixes oben;
|
||||
kompletter Ablauf inkl. aller aufgelaufenen UNGETESTET-Punkte steht
|
||||
|
||||
+46
-5
@@ -9,9 +9,32 @@
|
||||
cd "$(dirname "$0")"
|
||||
export PATH="$HOME/.local/bin:$HOME/flutter/bin:$PATH"
|
||||
|
||||
# Pyramid laeuft auf eigenem Claude-Konto (Profil ~/.claude-pyramid),
|
||||
# das Standard-Profil ~/.claude bleibt fuer die Webseiten-Arbeit.
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
|
||||
# --- Zwei Claude-Konten mit automatischem Wechsel bei 5h-Limit ---
|
||||
# Konto "pyramid" = Profil ~/.claude-pyramid (Standard fuer diese Arbeit),
|
||||
# Konto "haupt" = Profil ~/.claude (springt ein, wenn pyramid am Limit ist).
|
||||
ACCOUNT_FILE="$HOME/.claude-accounts/active"
|
||||
get_account() { cat "$ACCOUNT_FILE" 2>/dev/null || echo pyramid; }
|
||||
|
||||
apply_account() {
|
||||
if [ "$(get_account)" = "haupt" ]; then
|
||||
# Hauptkonto = Standard-Profil -> Variable NICHT setzen
|
||||
# (sonst sucht Claude die Config unter ~/.claude/.claude.json statt ~/.claude.json)
|
||||
unset CLAUDE_CONFIG_DIR
|
||||
else
|
||||
export CLAUDE_CONFIG_DIR="$HOME/.claude-pyramid"
|
||||
fi
|
||||
}
|
||||
|
||||
switch_account() {
|
||||
local cur next
|
||||
cur=$(get_account)
|
||||
if [ "$cur" = "pyramid" ]; then next=haupt; else next=pyramid; fi
|
||||
mkdir -p "$HOME/.claude-accounts"
|
||||
echo "$next" > "$ACCOUNT_FILE"
|
||||
notify_matrix "🔺 Autopilot: 5h-Limit auf Konto '$cur' erreicht – wechsle auf Konto '$next' und arbeite direkt weiter."
|
||||
}
|
||||
|
||||
JUST_SWITCHED=0
|
||||
|
||||
# --- Matrix-Statusberichte (Raum "🔺 Pyramid Autopilot", Token vom Gatus-Bot) ---
|
||||
MATRIX_URL="http://127.0.0.1:6167"
|
||||
@@ -36,9 +59,11 @@ while true; do
|
||||
sleep 600
|
||||
continue
|
||||
fi
|
||||
echo "===== $(date '+%F %T') Neue Claude-Session =====" | tee -a autopilot.log
|
||||
apply_account
|
||||
echo "===== $(date '+%F %T') Neue Claude-Session (Konto $(get_account)) =====" | tee -a autopilot.log
|
||||
BEFORE=$(git rev-parse HEAD 2>/dev/null)
|
||||
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log
|
||||
SESSION_OUT=$(mktemp)
|
||||
claude -p "$PROMPT" --model claude-fable-5 --dangerously-skip-permissions 2>&1 | tee -a autopilot.log "$SESSION_OUT"
|
||||
AFTER=$(git rev-parse HEAD 2>/dev/null)
|
||||
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$AFTER" ]; then
|
||||
COMMITS=$(git log --oneline --no-decorate "$BEFORE..$AFTER" | head -15)
|
||||
@@ -50,6 +75,22 @@ $COMMITS
|
||||
|
||||
$NEXT"
|
||||
fi
|
||||
# 5h-Limit erkannt? -> sofort aufs andere Konto wechseln statt zu warten
|
||||
if grep -qiE "hit your.*limit|usage limit|session limit" "$SESSION_OUT"; then
|
||||
rm -f "$SESSION_OUT"
|
||||
if [ "$JUST_SWITCHED" != "1" ]; then
|
||||
switch_account
|
||||
JUST_SWITCHED=1
|
||||
continue
|
||||
fi
|
||||
JUST_SWITCHED=0
|
||||
echo "===== $(date '+%F %T') Beide Konten am Limit - naechster Versuch in 20 Minuten =====" | tee -a autopilot.log
|
||||
sleep 1200
|
||||
continue
|
||||
fi
|
||||
rm -f "$SESSION_OUT"
|
||||
JUST_SWITCHED=0
|
||||
|
||||
echo "===== $(date '+%F %T') Session beendet - Neustart in 20 Minuten (Strg+C zum Stoppen) =====" | tee -a autopilot.log
|
||||
sleep 1200
|
||||
done
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# Dashboard-Server absichern: Admin-Endpoints haben KEINE Authentifizierung
|
||||
|
||||
**Status (ABGESCHLOSSEN 2026-07-05): Das Loch ist mit dem Heimnetz-Gate
|
||||
(„Weg C", siehe unten) geschlossen, und Bernd hat entschieden
|
||||
(`ANTWORTEN_BERND.md` Nr. 3): KEIN Fernzugriff – das Gate ist der ENDZUSTAND.**
|
||||
Die fünf Admin-Endpoints akzeptieren nur noch Anfragen aus dem
|
||||
Heimnetz/localhost; alles, was über den Cloudflare-Tunnel kommt, bekommt 403 mit
|
||||
einer Erklärung (die das Dashboard per `alert()` anzeigt). Bernds Dashboard
|
||||
funktioniert im Heimnetz unverändert unter
|
||||
`http://192.168.178.71:8080/stats.html`; unterwegs kommt Bernd per WireGuard ins
|
||||
Heimnetz. Die A/B-Frage (Cloudflare Access vs. `DASH_TOKEN`) ist damit
|
||||
GESCHLOSSEN – die Optionen unten bleiben nur als Archiv stehen, falls sich der
|
||||
Bedarf je ändert. Die unten dokumentierten Restrisiken des Gates sind bewusst
|
||||
akzeptiert. **Nicht weiter an diesem Thema arbeiten.**
|
||||
|
||||
## Heimnetz-Gate („Weg C", aktiv seit 2026-07-04, seit 2026-07-05 Endzustand)
|
||||
|
||||
In `server.py` (Konstante `ADMIN_PATHS` + Helfer `lan_denied_reason`): eine
|
||||
Admin-POST-Anfrage wird nur angenommen, wenn (1) **kein** `Cf-Connecting-Ip`/
|
||||
`Cf-Ray`-Header vorhanden ist (= sie kam nicht durch den Cloudflare-Tunnel) und
|
||||
(2) die **Socket-Absender-IP** (nicht spoofbar) `127.0.0.1`/`::1`/`192.168.*`
|
||||
ist. Abgelehnte Versuche landen größenbegrenzt (5 MB-Deckel) in
|
||||
`security_alerts.log` und damit in der Dashboard-Anzeige. Headless verifiziert:
|
||||
öffentliche URL → 403 (ban/toggle), Heimnetz-IP und localhost → App-Validierung
|
||||
erreicht (400 bei leerem Nutzer), gespoofter Cf-Header aus dem LAN → 403,
|
||||
`GET /` öffentlich → 200, `/api/livekit-token` (401 bei ungültigem Token) und
|
||||
`/api/e2ee-diagnostics` (403 bei falschem Token) unverändert öffentlich
|
||||
erreichbar (die App braucht sie, eigene Auth vorhanden).
|
||||
|
||||
**Bekannte Restrisiken des Gates (Bernd bekannt und bewusst akzeptiert,
|
||||
Entscheidung 2026-07-05):**
|
||||
- Die **öffentliche Ansicht bleibt offen** und zeigt Nutzerliste + Präsenz
|
||||
(Informations-Preisgabe). Weg A würde auch das schließen.
|
||||
- **CSRF aus dem Heimnetz:** Eine bösartige Webseite in Bernds Browser könnte
|
||||
theoretisch POSTs an `http://192.168.178.71:8080` schicken (moderne
|
||||
Chrome-Versionen blocken Public→Private-Requests, Firefox nicht vollständig).
|
||||
Erst ein Token/Access-Login (Weg A/B) schließt das sauber.
|
||||
- Wer **im Heimnetz** ist, kann weiterhin ohne Anmeldung administrieren.
|
||||
|
||||
## Problem (belegt, nicht vermutet)
|
||||
|
||||
Der Dashboard-Server `/home/steggi/matrix/server.py` (läuft als
|
||||
`matrix-stats.service` auf `0.0.0.0:8080`) ist über Cloudflare unter
|
||||
`https://dashboard.steggi-matrix.work` **öffentlich aus dem Internet erreichbar**.
|
||||
Getestet: `GET /` liefert 200 (stats.html), und die **zustandsändernden POST-Endpoints
|
||||
antworten mit der App-eigenen Validierung – ohne jede Authentifizierung**:
|
||||
|
||||
```
|
||||
$ curl -X POST https://dashboard.steggi-matrix.work/api/ban -d '{"user":""}'
|
||||
{"error": "Kein Nutzer angegeben"} # HTTP 400 – App-Antwort, KEIN Login davor
|
||||
```
|
||||
|
||||
Damit kann **jeder Fremde im Internet**, der den Hostnamen kennt, ohne Anmeldung:
|
||||
|
||||
- `/api/ban` / `/api/unban` – **jeden Matrix-Nutzer sperren/entsperren** (auch Uta!)
|
||||
→ Aussperrung / Denial-of-Service gegen echte Nutzer.
|
||||
- `/api/toggle-registration` und `/api/create-invite` – **offene Registrierung am
|
||||
Homeserver aktivieren** (setzt `allow_registration = true` +
|
||||
`yes_i_am_very_very_sure…`) → der Homeserver wird zur Spam-/Missbrauchsschleuder.
|
||||
- `/api/run-stats` – Stats-Skript auslösen (harmlos, aber ebenfalls ungeschützt).
|
||||
|
||||
**Der Hostname ist nicht wirklich geheim:** Cloudflare stellt TLS-Zertifikate aus,
|
||||
die in den öffentlichen Certificate-Transparency-Logs landen – jede Subdomain von
|
||||
`steggi-matrix.work` ist per CT-Log-Scan auffindbar. Die bisherige „Sicherheit"
|
||||
beruht also allein auf Verborgenheit eines Namens, der praktisch entdeckbar ist.
|
||||
|
||||
Bereits **korrekt abgesichert** (zum Vergleich, nicht betroffen):
|
||||
- `/api/e2ee-diagnostics` – eigener `DIAG_TOKEN` (Bearer).
|
||||
- `/api/livekit-token` – Matrix-Access-Token per `whoami` gegen Continuwuity.
|
||||
|
||||
Nur die vier Admin-/Stats-Endpoints oben sind offen.
|
||||
|
||||
## Wie das Dashboard heute aufruft
|
||||
|
||||
`stats.html` ruft alle Aktionen als **Same-Origin-`fetch` ohne Auth-Header** auf
|
||||
(`toggleRegistration`, `createInvite`, `refreshStats`, `doUserAction` →
|
||||
`/api/ban` /`/api/unban`). Es gibt keine Session, kein Cookie, keinen Token –
|
||||
deshalb bricht **jede** hinzugefügte Authentifizierung das Dashboard so lange,
|
||||
bis `stats.html` den Token mitschickt. Genau darum ist das eine Bernd-Entscheidung
|
||||
und kein stiller Fix.
|
||||
|
||||
## Option A (empfohlen, robust): Cloudflare Access vor die Subdomain
|
||||
|
||||
Am saubersten wird das Dashboard **komplett** durch Cloudflare Access geschützt
|
||||
(Zero-Trust-Login vor der Subdomain), statt in `server.py` Auth nachzubauen:
|
||||
|
||||
1. Im Cloudflare-Zero-Trust-Dashboard eine **Access-Application** für
|
||||
`dashboard.steggi-matrix.work` anlegen.
|
||||
2. Policy: nur Bernds E-Mail (`bernd.steckmeister@gmail.com`) per One-Time-PIN
|
||||
oder Google-Login zulassen.
|
||||
3. Fertig – `server.py` und `stats.html` bleiben **unverändert**, das Dashboard
|
||||
funktioniert für Bernd wie bisher (nach dem Access-Login), aber Fremde kommen
|
||||
gar nicht erst an die Endpoints.
|
||||
|
||||
Vorteil: kein Secret im Code, kein Umbau der HTML-Fetches, kein Aussperr-Risiko
|
||||
durch vergessene Tokens. Nachteil: Bernd muss die Access-App einmal im
|
||||
Cloudflare-Panel klicken (kein Code, aber sein Konto nötig). **Das ist der
|
||||
Königsweg – Option B nur, falls Access nicht gewünscht ist.**
|
||||
|
||||
## Option B (Fallback, rein in server.py): eigener Dashboard-Token
|
||||
|
||||
Wenn Access nicht gewünscht ist, ein **neuer, zufälliger `DASH_TOKEN`** (getrennt
|
||||
vom geleakten Admin-Token `J5lax…` und vom `DIAG_TOKEN`!), den die vier
|
||||
Admin-/Stats-Endpoints als `Authorization: Bearer <DASH_TOKEN>` verlangen:
|
||||
|
||||
```python
|
||||
# server.py, zu den anderen Token-Konstanten:
|
||||
DASH_TOKEN = "<neu erzeugen: python3 -c 'import secrets;print(\"dash-\"+secrets.token_urlsafe(24))'>"
|
||||
|
||||
def _dash_authed(self):
|
||||
return self.headers.get("Authorization", "") == "Bearer " + DASH_TOKEN
|
||||
```
|
||||
|
||||
In `do_POST` **vor** der Behandlung von `/api/ban`, `/api/unban`,
|
||||
`/api/toggle-registration`, `/api/create-invite`, `/api/run-stats` einfügen:
|
||||
|
||||
```python
|
||||
if self.path in ("/api/ban", "/api/unban", "/api/toggle-registration",
|
||||
"/api/create-invite", "/api/run-stats") and not self._dash_authed():
|
||||
self.send_error(403)
|
||||
return
|
||||
```
|
||||
|
||||
`GET /` und `/api/registration-status` bleiben öffentlich (nur Lesen), damit die
|
||||
Stats-Seite ohne Token sichtbar bleibt.
|
||||
|
||||
**`stats.html`** darf den Token **NIEMALS fest eingebettet** ausliefern (die Seite
|
||||
ist öffentlich – der Token läge sonst für jeden offen). Stattdessen einmal im
|
||||
Browser abfragen und in `localStorage` halten:
|
||||
|
||||
```javascript
|
||||
function dashToken() {
|
||||
var t = localStorage.getItem('dashToken');
|
||||
if (!t) { t = prompt('Dashboard-Token:'); if (t) localStorage.setItem('dashToken', t); }
|
||||
return t || '';
|
||||
}
|
||||
// und bei JEDEM Aktions-fetch:
|
||||
fetch('/api/ban', { method:'POST',
|
||||
headers: {'Content-Type':'application/json', 'Authorization':'Bearer ' + dashToken()},
|
||||
body: JSON.stringify({user:u}) })
|
||||
```
|
||||
|
||||
(betrifft `toggleRegistration`, `createInvite`, `refreshStats`, `doUserAction`).
|
||||
Bei 403 den gecachten Token verwerfen (`localStorage.removeItem('dashToken')`) und
|
||||
neu fragen. Bernd gibt den Token so genau **einmal pro Browser** ein.
|
||||
|
||||
## Rollout / Testplan
|
||||
|
||||
**Option A:** Access-App klicken → von einem fremden Netz/Inkognito prüfen, dass
|
||||
`https://dashboard.steggi-matrix.work/` zum Access-Login umleitet statt die Seite
|
||||
zu zeigen; dann als Bernd einloggen und alle Buttons testen. Kein Code-Deploy.
|
||||
|
||||
**Option B (headless auf dem Pi verifizierbar):**
|
||||
1. `server.py` + `stats.html` ändern, Dienst-Neustart (Prozess killen,
|
||||
`Restart=always` lädt neu – siehe PROGRESS 2026-07-04 zur polkit-Falle).
|
||||
2. `curl -X POST .../api/ban -d '{"user":"x"}'` **ohne** Bearer → **403** erwartet.
|
||||
3. Mit `Authorization: Bearer <DASH_TOKEN>` + leerem Nutzer → 400 (App-Validierung
|
||||
erreicht) → beweist: Token akzeptiert, Auth greift.
|
||||
4. Im Browser: Dashboard öffnen, Token eingeben, je ein Button testen
|
||||
(Registrierung toggeln + sofort zurück, Stats aktualisieren; Ban/Unban an
|
||||
einem **Test-Nutzer**, nicht an Uta).
|
||||
|
||||
## Warum erst nur der Plan kam – und dann doch das Interims-Gate
|
||||
|
||||
Eine Token-/Access-Absicherung (A/B) macht die Dashboard-Buttons überall
|
||||
funktionslos, bis Bernd aktiv etwas tut (Token eingeben bzw. Access-App
|
||||
einrichten) – das mitten in einer autonomen Session zu deployen verletzt die
|
||||
CLAUDE.md-Leitplanke „kein Aussperren". Das *Wie* der Anmeldung bleibt deshalb
|
||||
Bernds Richtungsentscheidung. Das später ergänzte Interims-Gate umgeht dieses
|
||||
Dilemma: Es braucht keinerlei Aktion von Bernd (Heimnetz-Nutzung läuft
|
||||
unverändert weiter, inkl. spontanem Spam-Bann von zu Hause), sperrt aber
|
||||
Fremde aus dem Internet sofort aus. Einzige Einschränkung: Admin-Buttons über
|
||||
die öffentliche URL gehen nicht – die 403-Meldung im Dashboard erklärt das und
|
||||
nennt die Heimnetz-URL. Mit Bernds Entscheidung vom 2026-07-05 (Heimnetz-only,
|
||||
unterwegs WireGuard) ist genau dieser Zustand der gewollte Endzustand. Rückbau
|
||||
(falls je nötig) = die Gate-Zeilen in `server.py` entfernen
|
||||
(Backup: `server.py.bak-20260704-dashgate`).
|
||||
@@ -0,0 +1,143 @@
|
||||
# LiveKit-Token server-seitig minten (apiSecret aus dem Client entfernen)
|
||||
|
||||
**Status:** geplant, NICHT umgesetzt (braucht echten Call-Test auf einem Gerät –
|
||||
Calls sind laut CLAUDE.md „heilig"). Dieser Plan ist gegen den echten Code und die
|
||||
echte Pi-Konfiguration geschrieben (2026-07-04), damit ein PC-Termin direkt starten
|
||||
kann. Analog zu `docs/SQLCIPHER_MIGRATION.md`.
|
||||
|
||||
## Problem
|
||||
|
||||
`lib/core/livekit_token.dart` mintet die LiveKit-JWTs **im Client**. Dafür muss das
|
||||
LiveKit-`apiSecret` im App-Code liegen (`livekit_token.dart:6`) – d. h. jeder
|
||||
App-Nutzer (auch Uta) trägt das SFU-Secret im Gerät. Wer es hat, kann sich für
|
||||
**jeden** Raum und **jede** Identität ein Beitritts-Token ausstellen. Das ist ein
|
||||
Architekturproblem, kein bloßer Repo-Leak: Selbst nach einem History-Rewrite bliebe
|
||||
das Secret im ausgelieferten Binary.
|
||||
|
||||
Aktueller Fluss (`lib/core/livekit_call_manager.dart:154`):
|
||||
|
||||
```dart
|
||||
final token = LiveKitTokenGenerator.generate(
|
||||
roomName: roomName,
|
||||
identity: identity,
|
||||
displayName: identity,
|
||||
ttlSeconds: 21600, // 6 h
|
||||
);
|
||||
```
|
||||
|
||||
Erzeugtes JWT (HS256, `livekit_token.dart:14-33`): Grants `video` mit
|
||||
`roomJoin/room/canPublish/canSubscribe/canPublishData`, `metadata` = Anzeigename,
|
||||
`iss` = apiKey (`LKMatrixPi`), `sub` = identity, `exp` = jetzt+ttl.
|
||||
|
||||
## Ziel
|
||||
|
||||
Das `apiSecret` liegt **nur noch auf dem Pi**. Der Client holt sich für einen
|
||||
konkreten Raum ein fertiges JWT von einem kleinen Token-Endpoint und übergibt es
|
||||
unverändert an `Room.connect`.
|
||||
|
||||
## Server-Seite (Pi)
|
||||
|
||||
Es gibt bereits einen schlanken Dashboard-Server ohne Fremd-Abhängigkeiten:
|
||||
`/home/steggi/matrix/server.py` (stdlib `http.server`, läuft als
|
||||
`matrix-stats.service` auf `0.0.0.0:8080`, nach außen `dashboard.steggi-matrix.work`).
|
||||
Der Token-Endpoint kann dort als weitere Route `POST /api/livekit-token` andocken –
|
||||
kein neuer Dienst nötig. Das LiveKit-`apiSecret` steht schon auf dem Pi in
|
||||
`/home/steggi/matrix/livekit.yaml` (`keys: LKMatrixPi: <secret>`); der Server liest
|
||||
es von dort, statt es erneut zu hinterlegen (eine Quelle der Wahrheit).
|
||||
|
||||
**Wichtig – Authentifizierung:** Der Endpoint darf NICHT anonym Tokens ausstellen
|
||||
(sonst ist nichts gewonnen). Der Client schickt seinen **Matrix-Access-Token** mit;
|
||||
der Server prüft ihn gegen Continuwuity und leitet daraus die Identität ab:
|
||||
|
||||
1. `POST /api/livekit-token`, Body `{ "room": "<roomName>", "matrix_token": "<access_token>" }`.
|
||||
2. Server ruft `GET http://127.0.0.1:6167/_matrix/client/v3/account/whoami`
|
||||
mit `Authorization: Bearer <matrix_token>` auf → liefert `user_id`
|
||||
(401 → Endpoint gibt 401 zurück, kein Token).
|
||||
3. `identity` = `user_id` (NICHT vom Client wählbar – verhindert Identitäts-Spoofing).
|
||||
4. Optional, aber empfohlen: Raummitgliedschaft prüfen (`/_matrix/client/v3/rooms/
|
||||
{roomId}/joined_members` oder der Matrix-Raum, zu dem der Voice-Channel gehört) –
|
||||
nur ausstellen, wenn der Nutzer wirklich Mitglied ist. Für Phase 1 reicht die
|
||||
whoami-Authentifizierung; Mitgliedschaftsprüfung als Härtung nachziehen.
|
||||
5. Server mintet das LiveKit-JWT **mit denselben Grants wie heute** und gibt
|
||||
`{ "token": "<jwt>", "url": "wss://livekit.steggi-matrix.work" }` zurück.
|
||||
|
||||
**JWT-Minting ohne Fremd-Paket (stdlib genügt):** LiveKit-Tokens sind HS256-JWTs.
|
||||
Mit `hmac`/`hashlib`/`base64`/`json` aus der stdlib:
|
||||
|
||||
```python
|
||||
import base64, hmac, hashlib, json, time
|
||||
|
||||
def _b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||
|
||||
def mint_livekit(api_key, api_secret, identity, room, name, ttl=21600):
|
||||
now = int(time.time())
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
payload = {
|
||||
"iss": api_key, "sub": identity, "name": name,
|
||||
"nbf": now, "exp": now + ttl, "metadata": name,
|
||||
"video": {"roomJoin": True, "room": room,
|
||||
"canPublish": True, "canSubscribe": True,
|
||||
"canPublishData": True},
|
||||
}
|
||||
segs = [_b64url(json.dumps(header, separators=(",", ":")).encode()),
|
||||
_b64url(json.dumps(payload, separators=(",", ":")).encode())]
|
||||
signing_input = b".".join(segs)
|
||||
sig = hmac.new(api_secret.encode(), signing_input, hashlib.sha256).digest()
|
||||
return (signing_input + b"." + _b64url(sig)).decode()
|
||||
```
|
||||
|
||||
Verifikation lokal auf dem Pi (kein Gerät nötig): geminetes Token gegen den
|
||||
laufenden LiveKit prüfen, z. B. per LiveKit-CLI `lk` oder indem man das JWT auf
|
||||
jwt.io-Art dekodiert und Claims/Signatur vergleicht. Zusätzlich Gegentest mit dem
|
||||
**alten** Client-Pfad: dasselbe Token muss `Room.connect` akzeptieren.
|
||||
|
||||
## Client-Seite
|
||||
|
||||
- `lib/core/livekit_token.dart`: `apiKey`/`apiSecret` **entfernen**. Die Klasse wird
|
||||
entweder gelöscht oder zu einem dünnen HTTP-Client
|
||||
`Future<String> fetchLiveKitToken({required String room, required String matrixToken})`,
|
||||
der `POST https://dashboard.steggi-matrix.work/api/livekit-token` aufruft und das
|
||||
`token`-Feld zurückgibt. `http`-Paket ist schon Abhängigkeit (siehe
|
||||
`update_checker.dart`).
|
||||
- `lib/core/livekit_call_manager.dart:154`: `LiveKitTokenGenerator.generate(...)`
|
||||
→ `await fetchLiveKitToken(room: roomName, matrixToken: matrixClient.accessToken)`.
|
||||
Der Manager hat den Matrix-Client bereits (`_matrixClient`, Zeile 149) – der
|
||||
Access-Token ist also verfügbar. `identity`/`displayName` werden nicht mehr vom
|
||||
Client bestimmt (Server leitet Identität aus whoami ab); den bisherigen
|
||||
`identity`-Parameter entsprechend zurückbauen.
|
||||
- Fehlerpfade: Netzfehler / 401 / 403 sauber als Call-Fehler anzeigen
|
||||
(`error = ...; notifyListeners()`), NICHT still schlucken – sonst „Call verbindet
|
||||
nicht" ohne Hinweis. TTL bleibt 21600 s (6 h), Erneuerung wie bisher beim
|
||||
Neu-Beitreten.
|
||||
|
||||
## Rotation (Pflicht, danach)
|
||||
|
||||
Das alte `apiSecret` `rYUT2PRa…` ist geleakt (Client + Git-History). Nach der
|
||||
Umstellung:
|
||||
|
||||
1. Neues `apiSecret` in `/home/steggi/matrix/livekit.yaml` (`keys:`) setzen.
|
||||
2. LiveKit-Container neu starten
|
||||
(`docker compose -f /home/steggi/matrix/docker-compose.yml restart livekit`).
|
||||
3. `server.py` liest das Secret aus `livekit.yaml` – kein zweiter Ort zu pflegen.
|
||||
4. Alte Clients (Utas installierte APK) minten mit dem alten Secret → deren
|
||||
selbst-gemintete Tokens werden nach der Rotation abgelehnt. Deshalb Rotation
|
||||
**zusammen mit einem neuen Release** ausrollen, das den neuen Fetch-Pfad nutzt.
|
||||
|
||||
## Reihenfolge / Testplan (PC + Gerät)
|
||||
|
||||
1. `server.py`-Route bauen, lokal auf dem Pi verifizieren (whoami-Auth greift,
|
||||
geminetes Token dekodiert korrekt, `Room.connect` akzeptiert es).
|
||||
2. Client umstellen, `flutter analyze` sauber, `flutter test` grün.
|
||||
3. **Auf echtem Gerät** (Calls heilig, kein Headless-Test möglich): Voice-Channel
|
||||
beitreten, Audio/Video/Screenshare, Verlassen/Wieder-Beitreten, zweiter
|
||||
Teilnehmer. Erst wenn das steht: Secret rotieren + Release.
|
||||
4. Danach ROADMAP-M0-Punkt „LiveKit-Token server-seitig minten" abhaken und den
|
||||
Rotations-Teil des Secret-Punkts als erledigt markieren.
|
||||
|
||||
## Warum nicht jetzt (auf dem Pi) gemacht
|
||||
|
||||
Die Server-Route ließe sich hier bauen und headless testen – aber der zugehörige
|
||||
Client-Umbau tauscht den **heiligen Call-Pfad** aus und ist auf diesem Pi ohne
|
||||
GUI/Gerät nicht praktisch prüfbar. Einen halben, nicht end-to-end verifizierbaren
|
||||
Umbau am Call-Pfad zu stapeln verbietet die CLAUDE.md-Leitplanke („keine
|
||||
Verhaltensänderung ohne Not", Calls heilig). Deshalb hier nur der fertige Plan.
|
||||
@@ -8,6 +8,13 @@ Punkt hier eintragen (✅/❌ + Datum), Befunde als eigene PROGRESS-Einträge.
|
||||
|
||||
Schnellster Weg: `flutter run -d windows`.
|
||||
|
||||
## 0. Vorab (1 Minute)
|
||||
|
||||
- [ ] `flutter test` – die Regressionstests unter `test/` (Soft-Logout-Guard,
|
||||
Power-Levels-Schutz, AuthLog, Storage-Fassade `settings_prefs` –
|
||||
Stand 2026-07-03: 24 Tests) müssen grün sein, bevor irgendetwas
|
||||
anderes geprüft wird.
|
||||
|
||||
## 1. Härtetest Login/Krypto (M1 – PFLICHT, zuerst!)
|
||||
|
||||
Deckt die Fixes 63e4919, 9dc83f7, 891f348 ab (Uta-Random-Logout + Guard).
|
||||
|
||||
@@ -70,7 +70,7 @@ Continuwuity hat einen speziellen Admin-Raum. Als Server-Admin kannst du dort Be
|
||||
# Als curl (Admin-Token erforderlich):
|
||||
# Admin-Room-ID herausfinden:
|
||||
curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3Asteggi-matrix.work" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
# → {"room_id": "!adminRoomId:steggi-matrix.work", ...}
|
||||
```
|
||||
|
||||
@@ -84,22 +84,22 @@ curl -s "https://steggi-matrix.work/_matrix/client/v3/directory/room/%23admins%3
|
||||
|
||||
# User zu Server-Admin machen
|
||||
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": "@todesneutron:steggi-matrix.work"}'
|
||||
|
||||
# Server-Status abfragen
|
||||
curl "https://steggi-matrix.work/_conduwuit/server/version" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
# Alternativ — Standard Matrix-Whoami (prüft ob Token gültig):
|
||||
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
# → {"user_id": "@todesneutron:steggi-matrix.work", "is_guest": false}
|
||||
|
||||
# Prüfen ob User Server-Admin ist (Conduwuit-spezifisch):
|
||||
curl "https://steggi-matrix.work/_conduwuit/admin/v1/users/@todesneutron:steggi-matrix.work" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -112,7 +112,7 @@ Als Server-Admin kannst du via Matrix Client API Power Levels in jedem Raum setz
|
||||
```bash
|
||||
# Schritt 1: Aktuelles Power-Level-Event lesen
|
||||
ROOM_ID="!raumId:steggi-matrix.work"
|
||||
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
TOKEN="<ADMIN_TOKEN>"
|
||||
SERVER="https://steggi-matrix.work"
|
||||
|
||||
curl "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
|
||||
@@ -172,7 +172,7 @@ await client.setRoomStateWithKey(
|
||||
```bash
|
||||
# Testen ob restricted join rules funktionieren:
|
||||
ROOM_ID="!testRaum:steggi-matrix.work"
|
||||
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
TOKEN="<ADMIN_TOKEN>"
|
||||
SPACE_ID="!spaceId:steggi-matrix.work"
|
||||
|
||||
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.join_rules/" \
|
||||
@@ -230,7 +230,7 @@ try {
|
||||
```bash
|
||||
# 1. Prüfen: Welchen Power Level hat mein User in diesem Raum?
|
||||
ROOM_ID="!raumId:steggi-matrix.work"
|
||||
TOKEN="J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
TOKEN="<ADMIN_TOKEN>"
|
||||
|
||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/${ROOM_ID}/state/m.room.power_levels/" \
|
||||
-H "Authorization: Bearer ${TOKEN}"
|
||||
@@ -269,7 +269,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/capabilities" \
|
||||
```bash
|
||||
# Prüfen ob User Server-Admin ist:
|
||||
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
# Auf Pi (SSH): User-Datenbank direkt abfragen (Conduwuit/Continuwuity nutzt RocksDB)
|
||||
# Alternativ: Über Admin-Room-Befehl
|
||||
@@ -278,7 +278,7 @@ curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
||||
|
||||
# Über HTTP (Conduwuit Admin-Endpoint):
|
||||
curl -X POST "https://steggi-matrix.work/_conduwuit/admin/v1/make_user_admin" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": "@todesneutron:steggi-matrix.work"}'
|
||||
```
|
||||
@@ -292,19 +292,19 @@ Da kein Synapse-Admin-API existiert, nutze die Standard-Matrix-Client-API mit Ad
|
||||
```bash
|
||||
# Raum beitreten (als Admin, auch wenn invite-only):
|
||||
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/join/!roomId%3Asteggi-matrix.work" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
|
||||
# User in Raum einladen:
|
||||
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/invite" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"user_id": "@user:steggi-matrix.work"}'
|
||||
|
||||
# Space-Child-Relation setzen (Channel einem Space hinzufügen):
|
||||
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Asteggi-matrix.work/state/m.space.child/!channelId%3Asteggi-matrix.work/" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"via": ["steggi-matrix.work"],
|
||||
@@ -313,7 +313,7 @@ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!spaceId%3Astegg
|
||||
|
||||
# Space-Parent-Relation setzen (im Channel-Raum):
|
||||
curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Asteggi-matrix.work/state/m.space.parent/!spaceId%3Asteggi-matrix.work/" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"via": ["steggi-matrix.work"],
|
||||
@@ -322,11 +322,11 @@ curl -X PUT "https://steggi-matrix.work/_matrix/client/v3/rooms/!channelId%3Aste
|
||||
|
||||
# Alle Mitglieder eines Raums auflisten:
|
||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/members" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
# Raum verlassen (als User, nicht löschen):
|
||||
curl -X POST "https://steggi-matrix.work/_matrix/client/v3/rooms/!roomId%3Asteggi-matrix.work/leave" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
@@ -419,15 +419,15 @@ Wenn du M_FORBIDDEN bekommst:
|
||||
```bash
|
||||
# 1. Token gültig?
|
||||
curl "https://steggi-matrix.work/_matrix/client/v3/account/whoami" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
# 2. PL im betroffenen Raum prüfen:
|
||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.power_levels/" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
# 3. Mitglied des Raums?
|
||||
curl "https://steggi-matrix.work/_matrix/client/v3/rooms/!ROOM_ID/state/m.room.member/%40todesneutron%3Asteggi-matrix.work/" \
|
||||
-H "Authorization: Bearer J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI"
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>"
|
||||
|
||||
# 4. Was genau schlägt fehl? (Antwort-Body lesen!)
|
||||
# M_FORBIDDEN + "Power level too low" → PL erhöhen
|
||||
|
||||
@@ -7,6 +7,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class BoolPref extends StateNotifier<bool> {
|
||||
final String _key;
|
||||
// Guards against the load race: if set()/toggle() ran before _load's read
|
||||
// lands, the stale persisted value must not overwrite the user's choice.
|
||||
bool _userSet = false;
|
||||
|
||||
BoolPref(this._key, bool defaultValue) : super(defaultValue) {
|
||||
_load(defaultValue);
|
||||
@@ -14,10 +17,11 @@ class BoolPref extends StateNotifier<bool> {
|
||||
|
||||
Future<void> _load(bool def) async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
if (mounted) state = p.getBool(_key) ?? def;
|
||||
if (mounted && !_userSet) state = p.getBool(_key) ?? def;
|
||||
}
|
||||
|
||||
Future<void> set(bool v) async {
|
||||
_userSet = true;
|
||||
state = v;
|
||||
final p = await SharedPreferences.getInstance();
|
||||
await p.setBool(_key, v);
|
||||
@@ -30,9 +34,10 @@ class BoolPref extends StateNotifier<bool> {
|
||||
|
||||
class StringSetPref extends StateNotifier<Set<String>> {
|
||||
final String _key;
|
||||
late final Future<void> _loaded;
|
||||
|
||||
StringSetPref(this._key) : super({}) {
|
||||
_load();
|
||||
_loaded = _load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
@@ -42,14 +47,18 @@ class StringSetPref extends StateNotifier<Set<String>> {
|
||||
}
|
||||
|
||||
Future<void> add(String v) async {
|
||||
if (state.contains(v)) return;
|
||||
// Read-modify-write: ohne abgeschlossenes Laden würde hier der halbleere
|
||||
// Start-State zurückgeschrieben und persistierte Einträge gingen verloren.
|
||||
await _loaded;
|
||||
if (!mounted || state.contains(v)) return;
|
||||
state = {...state, v};
|
||||
final p = await SharedPreferences.getInstance();
|
||||
await p.setStringList(_key, state.toList());
|
||||
}
|
||||
|
||||
Future<void> remove(String v) async {
|
||||
if (!state.contains(v)) return;
|
||||
await _loaded;
|
||||
if (!mounted || !state.contains(v)) return;
|
||||
state = {...state}..remove(v);
|
||||
final p = await SharedPreferences.getInstance();
|
||||
await p.setStringList(_key, state.toList());
|
||||
|
||||
@@ -428,7 +428,11 @@ class _EncryptionSectionState extends ConsumerState<_EncryptionSection> {
|
||||
final notifier = ref.read(e2eeDiagnosticsProvider.notifier);
|
||||
final msg = await notifier.upload(
|
||||
serverUrl: 'https://dashboard.steggi-matrix.work',
|
||||
token: 'J5laxTz5Zte9oRB5x8QsyYRQ6b0hAuXI',
|
||||
// Eng begrenzter Diagnose-Token (haengt nur an den Diagnose-Log an,
|
||||
// KEINE Admin-Rechte). Bewusst NICHT der Matrix-Server-Admin-Token –
|
||||
// der darf niemals im Client landen. Passendes Gegenstueck in
|
||||
// /home/steggi/matrix/server.py (DIAG_TOKEN) auf dem Pi.
|
||||
token: 'pyr-diag-6AyELgODRv-4rF4dcau3kSa5YbgZqUcn',
|
||||
);
|
||||
if (mounted) setState(() { _diagUploading = false; _diagMsgIsError = false; _diagMsg = msg; });
|
||||
} catch (e) {
|
||||
|
||||
+7
-7
@@ -801,10 +801,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -918,7 +918,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
@@ -1294,18 +1294,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
||||
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.16"
|
||||
version: "0.6.17"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -82,6 +82,10 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
# Nur für Tests: offizieller Weg, path_provider ohne echte Plattform zu
|
||||
# mocken (PathProviderPlatform.instance überschreiben). War schon transitiv
|
||||
# im Lockfile, kein neues externes Paket.
|
||||
path_provider_platform_interface: ^2.1.2
|
||||
|
||||
dependency_overrides:
|
||||
webcrypto:
|
||||
|
||||
+11
-1
@@ -11,7 +11,17 @@ Set-Location (Split-Path $PSScriptRoot -Parent)
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
$GiteaBase = "http://192.168.178.71:3000"
|
||||
$GiteaRepo = "steggi/pyramid"
|
||||
$GiteaToken = "bb522f9646c6856c9ab58bef0151ac36a9ce1d57"
|
||||
# Token NIE hier eintragen (CLAUDE.md: keine Secrets ins Repo). Er kommt aus der
|
||||
# Umgebungsvariable PYRAMID_GITEA_TOKEN – einmalig pro PC setzen mit:
|
||||
# [Environment]::SetEnvironmentVariable("PYRAMID_GITEA_TOKEN", "<token>", "User")
|
||||
# (neues Terminal öffnen, damit die Variable sichtbar ist)
|
||||
$GiteaToken = $env:PYRAMID_GITEA_TOKEN
|
||||
if ([string]::IsNullOrWhiteSpace($GiteaToken)) {
|
||||
Write-Error ("PYRAMID_GITEA_TOKEN ist nicht gesetzt. Einmalig setzen mit:`n" +
|
||||
' [Environment]::SetEnvironmentVariable("PYRAMID_GITEA_TOKEN", "<token>", "User")' +
|
||||
"`nDen Token in Gitea unter Einstellungen > Anwendungen erzeugen (Scope: repository).")
|
||||
exit 1
|
||||
}
|
||||
$Headers = @{ Authorization = "token $GiteaToken"; "Content-Type" = "application/json" }
|
||||
|
||||
# ── Read and Update version from pubspec.yaml ─────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
||||
import 'package:pyramid/core/auth_log.dart';
|
||||
|
||||
/// Plattform kaputt/nicht verfügbar (z. B. sehr früher App-Start, exotischer
|
||||
/// Fehler): genau der Fall, in dem AuthLog.write NIEMALS werfen darf – der
|
||||
/// Soft-Logout-Guard ruft es in seinem catch-Pfad auf, und ein Throw dort
|
||||
/// würde im SDK zu logout()+clear() eskalieren (der Uta-Bug).
|
||||
class _BrokenPathProvider extends PathProviderPlatform {
|
||||
@override
|
||||
Future<String?> getApplicationSupportPath() async =>
|
||||
throw StateError('Plattform nicht verfügbar');
|
||||
}
|
||||
|
||||
class _TempPathProvider extends PathProviderPlatform {
|
||||
_TempPathProvider(this.path);
|
||||
final String path;
|
||||
|
||||
@override
|
||||
Future<String?> getApplicationSupportPath() async => path;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// WICHTIG: Reihenfolge der Tests ist Absicht. AuthLog cached sein File-Handle
|
||||
// statisch; der Kaputt-Plattform-Test muss laufen, BEVOR ein Test mit
|
||||
// funktionierender Plattform das Handle in den Cache legt.
|
||||
late Directory tempDir;
|
||||
|
||||
setUpAll(() {
|
||||
tempDir = Directory.systemTemp.createTempSync('pyramid_auth_log_test');
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test(
|
||||
'write/read werfen NIE, auch wenn die Plattform fehlt '
|
||||
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden)',
|
||||
() async {
|
||||
PathProviderPlatform.instance = _BrokenPathProvider();
|
||||
// Darf normal zurückkehren, obwohl getApplicationSupportPath wirft.
|
||||
await AuthLog.write('darf nicht werfen');
|
||||
expect(await AuthLog.read(), startsWith('Log konnte nicht gelesen werden'));
|
||||
});
|
||||
|
||||
test('read ohne bisherige Einträge liefert Platzhalter statt Fehler',
|
||||
() async {
|
||||
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||
expect(await AuthLog.read(), '(noch keine Einträge)');
|
||||
});
|
||||
|
||||
test('write hängt Zeile mit ISO-Zeitstempel an, read liefert sie zurück',
|
||||
() async {
|
||||
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||
await AuthLog.write('Hallo Log');
|
||||
final content = await AuthLog.read();
|
||||
expect(content, contains('Hallo Log'));
|
||||
expect(
|
||||
RegExp(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}').hasMatch(content),
|
||||
isTrue,
|
||||
reason: 'jede Zeile beginnt mit einem ISO-8601-Zeitstempel',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'Log wird auf 500 Zeilen gekappt (älteste fliegen raus), '
|
||||
'damit es nie unbegrenzt wächst', () async {
|
||||
PathProviderPlatform.instance = _TempPathProvider(tempDir.path);
|
||||
for (var i = 0; i < 520; i++) {
|
||||
await AuthLog.write('Zeile $i');
|
||||
}
|
||||
final lines = (await AuthLog.read()).trimRight().split('\n');
|
||||
expect(lines.length, 500);
|
||||
expect(lines.last, contains('Zeile 519'),
|
||||
reason: 'neueste Einträge müssen überleben');
|
||||
expect((await AuthLog.read()).contains('Hallo Log'), isFalse,
|
||||
reason: 'ältester Eintrag (aus dem Test davor) muss weggekappt sein');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/features/spaces/space_admin_dialog.dart';
|
||||
|
||||
/// Fake-Client für `updatePowerLevelsSafely`: liefert einen vorgegebenen
|
||||
/// Server-Stand des power_levels-Events und zeichnet auf, was (wenn
|
||||
/// überhaupt) zurückgeschrieben wird. Alles andere → NoSuchMethodError.
|
||||
class _FakeClient implements Client {
|
||||
_FakeClient({required this.serverState, this.getError});
|
||||
|
||||
Map<String, Object?> serverState;
|
||||
MatrixException? getError;
|
||||
Map<String, Object?>? written;
|
||||
|
||||
@override
|
||||
String? get userID => '@me:pyramid.example';
|
||||
|
||||
@override
|
||||
Future<Map<String, Object?>> getRoomStateWithKey(
|
||||
String roomId,
|
||||
String eventType,
|
||||
String stateKey, {
|
||||
Format? format,
|
||||
}) async {
|
||||
final err = getError;
|
||||
if (err != null) throw err;
|
||||
return serverState;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> setRoomStateWithKey(
|
||||
String roomId,
|
||||
String eventType,
|
||||
String stateKey,
|
||||
Map<String, Object?> body,
|
||||
) async {
|
||||
written = body;
|
||||
return r'$neuesEvent';
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _FakeRoom implements Room {
|
||||
_FakeRoom(this.client);
|
||||
|
||||
@override
|
||||
final Client client;
|
||||
|
||||
@override
|
||||
String get id => '!raum:pyramid.example';
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Server-Stand bleibt erhalten: fremde Einträge werden nie überschrieben '
|
||||
'(genau der Bug, der schon einmal Admins ausgesperrt hat)', () async {
|
||||
final client = _FakeClient(serverState: {
|
||||
'users': {'@me:pyramid.example': 100, '@uta:pyramid.example': 50},
|
||||
'users_default': 0,
|
||||
'state_default': 50,
|
||||
});
|
||||
final room = _FakeRoom(client);
|
||||
|
||||
await updatePowerLevelsSafely(room, (content) {
|
||||
(content['users'] as Map<String, dynamic>)['@neu:pyramid.example'] = 50;
|
||||
});
|
||||
|
||||
final users = client.written!['users'] as Map;
|
||||
expect(users['@uta:pyramid.example'], 50,
|
||||
reason: 'bestehender Server-Eintrag muss überleben');
|
||||
expect(users['@me:pyramid.example'], 100);
|
||||
expect(users['@neu:pyramid.example'], 50);
|
||||
});
|
||||
|
||||
test('Selbst-Degradierung unter das nötige Level wird abgebrochen, '
|
||||
'nichts wird geschrieben', () async {
|
||||
final client = _FakeClient(serverState: {
|
||||
'users': {'@me:pyramid.example': 100},
|
||||
'state_default': 50,
|
||||
});
|
||||
final room = _FakeRoom(client);
|
||||
|
||||
await expectLater(
|
||||
updatePowerLevelsSafely(room, (content) {
|
||||
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 0;
|
||||
}),
|
||||
throwsException,
|
||||
);
|
||||
expect(client.written, isNull);
|
||||
});
|
||||
|
||||
test('eigener Eintrag entfernt → Fallback auf users_default greift und '
|
||||
'schützt ebenfalls', () async {
|
||||
final client = _FakeClient(serverState: {
|
||||
'users': {'@me:pyramid.example': 100},
|
||||
'users_default': 0,
|
||||
});
|
||||
final room = _FakeRoom(client);
|
||||
|
||||
await expectLater(
|
||||
updatePowerLevelsSafely(room, (content) {
|
||||
(content['users'] as Map<String, dynamic>)
|
||||
.remove('@me:pyramid.example');
|
||||
}),
|
||||
throwsException,
|
||||
);
|
||||
expect(client.written, isNull);
|
||||
});
|
||||
|
||||
test('explizites events[m.room.power_levels]-Erfordernis wird respektiert',
|
||||
() async {
|
||||
final client = _FakeClient(serverState: {
|
||||
'users': {'@me:pyramid.example': 60},
|
||||
'events': {'m.room.power_levels': 75},
|
||||
'state_default': 50,
|
||||
});
|
||||
final room = _FakeRoom(client);
|
||||
|
||||
// 60 < 75 → muss abbrechen, obwohl state_default (50) erfüllt wäre.
|
||||
await expectLater(
|
||||
updatePowerLevelsSafely(room, (_) {}),
|
||||
throwsException,
|
||||
);
|
||||
expect(client.written, isNull);
|
||||
});
|
||||
|
||||
test('fehlendes power_levels-Event (M_NOT_FOUND): Start mit leerem Event, '
|
||||
'Schreiben nur wenn man sich selbst hoch genug setzt', () async {
|
||||
final client = _FakeClient(
|
||||
serverState: {},
|
||||
getError: MatrixException.fromJson(
|
||||
{'errcode': 'M_NOT_FOUND', 'error': 'Event not found.'}),
|
||||
);
|
||||
final room = _FakeRoom(client);
|
||||
|
||||
// Ohne eigenen Eintrag: users_default 0 < state_default-Fallback 50 → Abbruch.
|
||||
await expectLater(
|
||||
updatePowerLevelsSafely(room, (_) {}),
|
||||
throwsException,
|
||||
);
|
||||
expect(client.written, isNull);
|
||||
|
||||
// Setzt man sich selbst auf 100, darf geschrieben werden.
|
||||
await updatePowerLevelsSafely(room, (content) {
|
||||
(content['users'] as Map<String, dynamic>)['@me:pyramid.example'] = 100;
|
||||
});
|
||||
final users = client.written!['users'] as Map;
|
||||
expect(users['@me:pyramid.example'], 100);
|
||||
});
|
||||
|
||||
test('andere Serverfehler (z. B. M_FORBIDDEN) werden durchgereicht, '
|
||||
'nichts wird geschrieben', () async {
|
||||
final client = _FakeClient(
|
||||
serverState: {},
|
||||
getError: MatrixException.fromJson(
|
||||
{'errcode': 'M_FORBIDDEN', 'error': 'Nope.'}),
|
||||
);
|
||||
final room = _FakeRoom(client);
|
||||
|
||||
await expectLater(
|
||||
updatePowerLevelsSafely(room, (_) {}),
|
||||
throwsA(isA<MatrixException>()),
|
||||
);
|
||||
expect(client.written, isNull);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pyramid/core/settings_prefs.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Tests für die Storage-Fassade, die laut M2-Modulschnitt zur Pflicht-
|
||||
/// Schnittstelle für alle Einstellungs-Zugriffe ausgebaut werden soll –
|
||||
/// dieses Vertragsnetz muss bei dem Umbau stehen bleiben.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
// Frischer In-Memory-Store pro Test (setzt auch die gecachte
|
||||
// SharedPreferences-Instanz zurück).
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('BoolPref', () {
|
||||
test('lädt persistierten Wert und überstimmt damit den Default', () async {
|
||||
SharedPreferences.setMockInitialValues({'notif_sound': false});
|
||||
final pref = BoolPref('notif_sound', true);
|
||||
expect(pref.state, true, reason: 'Default gilt, bis geladen ist');
|
||||
await pumpEventQueue();
|
||||
expect(pref.state, false, reason: 'persistierter Wert muss gewinnen');
|
||||
});
|
||||
|
||||
test(
|
||||
'set() sofort nach Erstellung gewinnt gegen den später ladenden '
|
||||
'persistierten Wert (Lade-Race)', () async {
|
||||
SharedPreferences.setMockInitialValues({'notif_sound': false});
|
||||
final pref = BoolPref('notif_sound', false);
|
||||
final done = pref.set(true); // bevor _load fertig ist
|
||||
await pumpEventQueue();
|
||||
await done;
|
||||
expect(pref.state, true,
|
||||
reason: 'die Nutzeraktion darf nicht vom Lade-Ergebnis '
|
||||
'zurücküberschrieben werden');
|
||||
final p = await SharedPreferences.getInstance();
|
||||
expect(p.getBool('notif_sound'), true);
|
||||
});
|
||||
|
||||
test('set/toggle ändern Zustand und persistieren', () async {
|
||||
final pref = BoolPref('notif_desktop', true);
|
||||
await pumpEventQueue();
|
||||
pref.toggle();
|
||||
await pumpEventQueue();
|
||||
expect(pref.state, false);
|
||||
final p = await SharedPreferences.getInstance();
|
||||
expect(p.getBool('notif_desktop'), false);
|
||||
});
|
||||
});
|
||||
|
||||
group('StringSetPref', () {
|
||||
test('lädt persistierte Liste als Set, add/remove persistieren', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'muted_rooms': ['!a:hs', '!b:hs'],
|
||||
});
|
||||
final pref = StringSetPref('muted_rooms');
|
||||
await pumpEventQueue();
|
||||
expect(pref.state, {'!a:hs', '!b:hs'});
|
||||
|
||||
await pref.add('!c:hs');
|
||||
await pref.remove('!a:hs');
|
||||
final p = await SharedPreferences.getInstance();
|
||||
expect(p.getStringList('muted_rooms')!.toSet(), {'!b:hs', '!c:hs'});
|
||||
});
|
||||
|
||||
test(
|
||||
'add() sofort nach Erstellung verliert keine bereits persistierten '
|
||||
'Einträge (Lade-Race)', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'muted_rooms': ['!a:hs'],
|
||||
});
|
||||
final pref = StringSetPref('muted_rooms');
|
||||
final done = pref.add('!b:hs'); // bevor _load fertig ist
|
||||
await pumpEventQueue();
|
||||
await done;
|
||||
expect(pref.state, {'!a:hs', '!b:hs'});
|
||||
final p = await SharedPreferences.getInstance();
|
||||
expect(p.getStringList('muted_rooms')!.toSet(), {'!a:hs', '!b:hs'},
|
||||
reason: 'Read-Modify-Write auf ungeladenem State darf keine '
|
||||
'persistierten Einträge verlieren');
|
||||
});
|
||||
|
||||
test('doppeltes add und remove von Fehlendem sind No-Ops', () async {
|
||||
final pref = StringSetPref('muted_rooms');
|
||||
await pumpEventQueue();
|
||||
await pref.add('!a:hs');
|
||||
await pref.add('!a:hs');
|
||||
await pref.remove('!gibtsnicht:hs');
|
||||
expect(pref.state, {'!a:hs'});
|
||||
});
|
||||
});
|
||||
|
||||
group('Theme-Prefs', () {
|
||||
test('leerer Store liefert die dokumentierten Defaults', () async {
|
||||
final t = await loadThemePrefs();
|
||||
expect(t.isDark, true);
|
||||
expect(t.accentIdx, 0);
|
||||
expect(t.radius, 12.0);
|
||||
expect(t.motion, 1.0);
|
||||
});
|
||||
|
||||
test('Roundtrip: gespeicherte Werte kommen zurück, Teil-Save lässt '
|
||||
'den Rest unangetastet', () async {
|
||||
await saveThemePrefs(isDark: false, accentIdx: 3, radius: 8, motion: 0.5);
|
||||
var t = await loadThemePrefs();
|
||||
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 3, 8.0, 0.5));
|
||||
|
||||
// Nur ein Feld ändern – die anderen dürfen nicht zurückfallen.
|
||||
await saveThemePrefs(accentIdx: 1);
|
||||
t = await loadThemePrefs();
|
||||
expect((t.isDark, t.accentIdx, t.radius, t.motion), (false, 1, 8.0, 0.5));
|
||||
});
|
||||
});
|
||||
|
||||
group('Server-Banner', () {
|
||||
test('Roundtrip + Entfernen per null/leerem mxcUri', () async {
|
||||
await saveServerBanner('https://hs1', 'mxc://hs1/abc');
|
||||
await saveServerBanner('https://hs2', 'mxc://hs2/def');
|
||||
expect(await loadAllServerBanners(),
|
||||
{'https://hs1': 'mxc://hs1/abc', 'https://hs2': 'mxc://hs2/def'});
|
||||
|
||||
await saveServerBanner('https://hs1', null);
|
||||
await saveServerBanner('https://hs2', '');
|
||||
expect(await loadAllServerBanners(), isEmpty);
|
||||
});
|
||||
|
||||
test('korruptes JSON im Store crasht nicht, sondern liefert leere Map',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues(
|
||||
{'server_banners': '{kein json'});
|
||||
expect(await loadAllServerBanners(), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/soft_logout_guard.dart';
|
||||
|
||||
/// Fake-Client: nur `refreshAccessToken` ist implementiert. Jeder andere
|
||||
/// SDK-Aufruf (insbesondere `logout()`/`clear()`!) schlägt laut mit
|
||||
/// NoSuchMethodError fehl – so würde der Test es sofort merken, wenn der
|
||||
/// Guard je etwas anderes am Client aufriefe als den Refresh.
|
||||
class _FakeClient implements Client {
|
||||
_FakeClient(this._onRefresh);
|
||||
|
||||
final Future<void> Function(int attempt) _onRefresh;
|
||||
int attempts = 0;
|
||||
|
||||
@override
|
||||
Future<void> refreshAccessToken({Duration? customRefreshTokenLifetime}) {
|
||||
attempts++;
|
||||
return _onRefresh(attempts);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('erfolgreicher Refresh beim ersten Versuch: genau 1 Aufruf', () async {
|
||||
final client = _FakeClient((_) async {});
|
||||
await guardedSoftLogoutRefresh(client);
|
||||
expect(client.attempts, 1);
|
||||
});
|
||||
|
||||
test(
|
||||
'transienter Fehler (z. B. Netz weg / Token-Rotations-Race mit dem '
|
||||
'Push-Isolate): Retry heilt, kein Fehler nach außen',
|
||||
() async {
|
||||
final client = _FakeClient((attempt) async {
|
||||
if (attempt == 1) throw Exception('Netzwerkfehler');
|
||||
});
|
||||
await guardedSoftLogoutRefresh(client);
|
||||
expect(client.attempts, 2);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'dauerhafter Fehler: genau 3 Versuche, und der Guard wirft NIE '
|
||||
'(CLAUDE.md: Fehlerpfade dürfen nie in einem stillen Logout enden – '
|
||||
'ein Throw hier würde im SDK zu logout()+clear() eskalieren)',
|
||||
() async {
|
||||
final client = _FakeClient((_) async {
|
||||
throw Exception('Server dauerhaft nicht erreichbar');
|
||||
});
|
||||
// Darf trotz 3 Fehlversuchen normal (ohne Exception) zurückkehren.
|
||||
await guardedSoftLogoutRefresh(client);
|
||||
expect(client.attempts, 3);
|
||||
},
|
||||
// Der Guard wartet real 2s+4s zwischen den Versuchen.
|
||||
timeout: const Timeout(Duration(seconds: 30)),
|
||||
);
|
||||
|
||||
test('auch ein Error (nicht nur Exception) im Refresh entkommt nie', () async {
|
||||
final client = _FakeClient((_) async {
|
||||
throw StateError('unerwarteter Zustand');
|
||||
});
|
||||
await guardedSoftLogoutRefresh(client);
|
||||
expect(client.attempts, 3);
|
||||
}, timeout: const Timeout(Duration(seconds: 30)));
|
||||
}
|
||||
Reference in New Issue
Block a user