From 5139ade7e09f7e0a7deefe99c6211c61113368fb Mon Sep 17 00:00:00 2001 From: Till JS Date: Mon, 20 Apr 2026 20:38:55 +0200 Subject: [PATCH] feat(spaces): invite + accept flow (member management UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First user-facing surface for multi-tenant Space sharing. Two new routes: /spaces/members — Space member management (inside app gate) - Lists current members with role chips + avatars. - Einladen-Form for owners/admins: email + role (member/admin) → POST /api/auth/organization/invite-member. Better Auth's existing sendInvitationEmail handler (wired in better-auth.config.ts) mails the invitee a link to /accept-invitation?id=X. - Pending-invitations list with Stornieren button. - Personal Spaces show a hint panel instead — they can't have members by design. - Remove Mitglied button (not for owner-role). /accept-invitation — landing page for the invite email link (outside (app) guard so logged-out invitees can see it). - Fetches invitation details via /organization/get-invitation. - If unauthenticated: "Einloggen & annehmen" routes through /login with a callbackURL back to the landing — the flow resumes after sign-in. - Accept: POST /organization/accept-invitation + /set-active so the newly-joined space is active when the user lands in the app. - Decline: POST /organization/reject-invitation. - Already-accepted / expired / canceled states each get their own copy. SpaceSwitcher gets a "Mitglieder verwalten …" entry in the dropdown, visible only when the active Space isn't personal. What this does NOT do yet (separate commits): - Membership-Lookup in mana-sync — Users A and B can now be in the same space on paper, but mana-sync's RLS only lets members see their own authored records until the lookup is wired. - Encryption skip for shared-space rows — records in an encrypted table still get wrapped with the author's user key, so member B can't decrypt member A's writes. Both follow in the next two commits. 0 errors across 7194 files. Plan: docs/plans/spaces-foundation.md Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/layout/SpaceSwitcher.svelte | 9 + .../routes/(app)/spaces/members/+page.svelte | 491 ++++++++++++++++++ .../src/routes/accept-invitation/+page.svelte | 307 +++++++++++ 3 files changed, 807 insertions(+) create mode 100644 apps/mana/apps/web/src/routes/(app)/spaces/members/+page.svelte create mode 100644 apps/mana/apps/web/src/routes/accept-invitation/+page.svelte diff --git a/apps/mana/apps/web/src/lib/components/layout/SpaceSwitcher.svelte b/apps/mana/apps/web/src/lib/components/layout/SpaceSwitcher.svelte index 3e775f277..f2a2620ca 100644 --- a/apps/mana/apps/web/src/lib/components/layout/SpaceSwitcher.svelte +++ b/apps/mana/apps/web/src/lib/components/layout/SpaceSwitcher.svelte @@ -144,6 +144,11 @@ {/each} {/if}
+ {#if active && active.type !== 'personal'} + (open = false)}> + {locale === 'de' ? 'Mitglieder verwalten …' : 'Manage members …'} + + {/if} + + {#if inviteError}

{inviteError}

{/if} + {#if inviteSuccess}

{inviteSuccess}

{/if} + + {/if} + +
+

Mitglieder ({members.length})

+ {#if loading} +

Lädt …

+ {:else if loadError} +

{loadError}

+ {:else if members.length === 0} +

Nur du bist Mitglied.

+ {:else} +
    + {#each members as m (m.id)} +
  • +
    + +
    +
    {m.user?.name ?? m.user?.email ?? m.userId}
    + {#if m.user?.email && m.user?.name} +
    {m.user.email}
    + {/if} +
    +
    +
    + {m.role} + {#if canManage && m.role !== 'owner'} + + {/if} +
    +
  • + {/each} +
+ {/if} +
+ + {#if invitations.length > 0} +
+

Offene Einladungen ({invitations.length})

+
    + {#each invitations.filter((i) => i.status === 'pending') as inv (inv.id)} +
  • +
    +
    {inv.email}
    +
    + {inv.role} · verschickt {relativeDate(inv.expiresAt)} +
    +
    + {#if canManage} + + {/if} +
  • + {/each} +
+
+ {/if} + {/if} + + + diff --git a/apps/mana/apps/web/src/routes/accept-invitation/+page.svelte b/apps/mana/apps/web/src/routes/accept-invitation/+page.svelte new file mode 100644 index 000000000..7d0a21ce7 --- /dev/null +++ b/apps/mana/apps/web/src/routes/accept-invitation/+page.svelte @@ -0,0 +1,307 @@ + + +
+
+ {#if loading} +

Lade Einladung …

+ {:else if loadError} +

Einladung nicht abrufbar

+

{loadError}

+

Der Link ist möglicherweise abgelaufen oder schon benutzt.

+ {:else if invitation} + {#if invitation.status === 'accepted'} +

Schon angenommen

+

Diese Einladung ist bereits angenommen worden.

+ Zur App + {:else if invitation.status === 'rejected' || invitation.status === 'canceled'} +

Einladung abgelaufen

+

Diese Einladung ist nicht mehr gültig.

+ {:else} +

Einladung

+

+ {invitation.inviterEmail ?? 'Jemand'} lädt dich in + {invitation.organizationName ?? 'einen Space'} ein +

+

+ {SPACE_TYPE_LABELS.de[spaceType]} + Rolle: {invitation.role} +

+

+ Nach Annahme kannst du in diesem Space mitarbeiten — sehen, was andere schreiben, und + selbst Einträge anlegen. Deine persönlichen Daten bleiben in deinem Personal-Space, + getrennt. +

+ {#if actionError}

{actionError}

{/if} +
+ + +
+ {/if} + {/if} +
+
+ +