managarten/docs/plans/shared-space-smoketest.md
Till JS 38d35247cd feat(spaces): end-to-end shared-space sync (membership lookup + plaintext)
Closes the gap between "invite flow UI exists" and "two users in the
same space actually see each other's data". Three pieces land together
because they're meaningless without each other.

mana-auth — new internal endpoint:
  GET /api/v1/internal/users/:userId/memberships
  Returns [{organizationId, role}, ...] for the user. mana-sync uses
  this to populate the multi-member RLS session config.

mana-sync — membership lookup:
  new internal/memberships package with an HTTP client + 5 min
  per-user cache, fail-open (empty list = pre-Spaces behavior).
  Config gets MANA_AUTH_URL (default http://localhost:3001).
  Handler.NewHandler takes the Lookup. Every Push/Pull/Stream call
  now passes spaceIDsFor(userID) to Store methods.
  GetChangesSince + GetAllChangesSince extend their WHERE clause:
    WHERE (user_id = $1 OR space_id = ANY($memberSpaces))
  so co-members see each other's rows, not just the author.

apps/web — encryption skip for shared-space records:
  encryptRecord now checks record.spaceId:
    - `_personal:<userId>` sentinel OR no active shared space → encrypt
      with user master key (E2E as today).
    - Active space resolves to non-personal type AND spaceId matches
      that space → skip encryption; write lands plaintext.
  decryptRecord is unchanged because its per-field isEncrypted() guard
  already passes plaintext through.
  Phase-1 compromise: shared-space data is protected by server RLS
  only, not E2E. Phase 2 adds per-Space shared keys with per-member
  wrap — tracked in docs/plans/spaces-foundation.md.

Plus docs/plans/shared-space-smoketest.md: step-by-step Zwei-User-Test
mit erwarteten Ergebnissen und Debugging-Hinweisen bei Problemen.

Build + go test + web check all green.

Plan: docs/plans/spaces-foundation.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:46:53 +02:00

5.5 KiB
Raw Blame History

Shared-Space Smoketest

Schritt-für-Schritt-Anleitung, um das Shared-Space-Ende zu validieren. Zwei Nutzer, ein gemeinsamer Space, echtes Sync + Einladungsflow.

Vorbereitung

Lokaler Stack läuft (pnpm docker:up && pnpm run mana:dev). Vor dem Test einmal sicherstellen:

  1. DB-Migrationen aktuell:

    cd services/mana-auth && bun run db:push
    

    Holt das spaces-Schema (v004) mit credentials + module_permissions. mana-sync's eigene Migration (inkl. multi-member RLS) läuft beim Service-Start automatisch.

  2. Dev-User vorhanden:

    pnpm setup:dev-user
    

    Legt drei Accounts an (tills95@gmail.com, tilljkb@gmail.com, rajiehq@gmail.com), alle mit Passwort Aa-123456789 und Founder-Tier. Durch den Signup-Hook bekommt jeder automatisch seinen Personal-Space.

  3. SMTP-Stub aktiv (für Invite-Mails): Stalwart SMTP läuft im docker-compose.macmini.yml-Stack. Lokal kann man den Invite-Link auch direkt aus den Logs von mana-auth ziehen, falls die Mail nicht ankommt.

Szenario: Familie

Zwei getrennte Browser-Profile (oder ein normales + ein Inkognito-Fenster). Ohne Profil-Trennung teilen sich die Sessions die Cookies, und der Test ist wertlos.

User A (Besitzer)

  1. Einloggen als tills95@gmail.com
  2. Oben rechts im Space-Switcher "+ Neuer Space" klicken
  3. Typ Familie, Name "Schmidt Family", Slug automatisch
  4. Erstellen → Reload in den neuen Space, Switcher zeigt "Schmidt Family" + grünes Family-Badge
  5. Im Switcher → "Mitglieder verwalten …"
  6. Einladen-Formular: tilljkb@gmail.com als Mitglied → Senden
  7. Erfolgsmeldung "Einladung an … gesendet", Invite erscheint in "Offene Einladungen"

User B (Eingeladener)

  1. E-Mail öffnen, Einladungslink anklicken (/accept-invitation?id=…)
  2. Falls nicht eingeloggt: "Einloggen & annehmen" — bringt zu Login, nach erfolgreicher Authentifizierung automatisch zurück auf Accept-Seite
  3. Space-Vorschau zeigt: "tills95@gmail.com lädt dich in Schmidt Family ein", Typ-Badge "Familie", Rolle "Mitglied"
  4. Annehmen klicken → Redirect auf /, Space-Switcher oben rechts zeigt jetzt "Schmidt Family"

Gemeinsamer Test (wichtig!)

  1. User A legt im Familien-Space ein Event für morgen an (Kalender-Modul)
  2. User B öffnet den Kalender — das Event erscheint nach ≤1 s
  3. User B legt ein Rezept an
  4. User A sieht das Rezept nach ≤1 s

Isolation-Check (wichtig!)

  1. User A wechselt auf den Personal-Space (Switcher)
  2. User A trägt einen privaten Mood-Eintrag ein
  3. User B wechselt auf den Familien-Space und öffnet den Kalender-Modul — User A's Personal-Daten dürfen nicht sichtbar sein. Mood-Modul ist im Familien-Space eh nicht freigeschaltet (SPACE_MODULE_ALLOWLIST).

Rücktritt-Test

  1. User A öffnet "Mitglieder verwalten …" und entfernt User B
  2. User B lädt die Seite neu — Familien-Space ist verschwunden, aktiver Space fällt zurück auf Personal

Was dabei geprüft wird

Feature Erwartung
Invite-Mail versandt Stalwart-Log zeigt Mail, oder E-Mail-Inbox hat Link
Better-Auth accept-invitation auth.invitations.statusaccepted
Member-Datensatz auth.members hat neuen Row für User B × org
activeOrganizationId gesetzt Session-Cookie zeigt den neuen Space
Scope-Filter greift User B sieht keine Personal-Daten von User A
Multi-Member-RLS greift User B sieht neue Events/Rezepte von User A
Membership-Lookup in mana-sync Keine 404s im mana-sync-Log beim Zugriff
Encryption-Skip Records im Familien-Space werden plaintext synced (prüfen via DevTools → IndexedDB)
Rollen-Enforcement User B kann nicht weitere einladen (Rolle=Mitglied, canManage=false)

Bekannte Phase-1-Einschränkungen

  • Daten im Shared-Space sind unverschlüsselt (nur RLS-geschützt) — im RFC docs/plans/spaces-foundation.md als bewusster Phase-1-Kompromiss dokumentiert. Phase 2 addiert einen pro-Space-Schlüssel mit Per-Member-Wrap.
  • Subscription-Fan-Out läuft noch per-User — User A's Write triggert Mana-Sync's Pull erst nach seinem eigenen Tick; User B sieht den Eintrag nach bis zu 1 s, nicht instant. Sobald Fan-Out steht, ist das Sub-200ms.
  • Role-Änderungen brauchen Cache-Invalidierung: nach Role-Update dauert die mana-sync-Membership-Cache bis zu 5 min. Für Tests nach dem Leave einfach den Service neu starten.

Was tun wenn's nicht klappt

  • E-Mail kommt nicht an: in mana-auth-Logs nach accept-invitation?id= suchen — Better Auth loggt den Link bei sendInvitationEmail.
  • User B sieht Space nicht nach Accept: Browser-DevTools → Cookies → mana.session_data auf die neue Session-ID prüfen, dann loadActiveSpace in der Konsole forcen: await (await import('/src/lib/data/scope/index.ts')).loadActiveSpace({ force: true })
  • Rezepte erscheinen bei A nicht: mana-sync-Logs auf [memberships] oder Policy-Denials checken. app.current_user_space_ids sollte die Schmidt-Family-Org-ID enthalten.
  • Kein Access zu Mitglieder-Seite: Personal-Space kann per Design keine Mitglieder haben — vorher auf den Familien-Space wechseln.

Nächste Schritte nach Smoketest

Wenn alle 9+ Checks grün sind: Foundation ist validated. Follow-ups (nicht blockierend):

  • Echtes WebSocket-Fan-Out an Space-Mitglieder
  • Shared-Space-Encryption mit per-Member-Key-Wrap
  • Member-Activity-Log (wer hat wann was geändert)
  • Rollen-spezifische Permissions (trainer darf kein club-finance)