543e9709be
Security - Reject URL :id when it does not match the caller's active tenant on every /tenants/:id/members/* and /tenants/:id/leave endpoint. Without this an Owner of tenant A could drive operations on tenant B simply by changing the URL, because RequireRole resolves the caller's role against the active tenant from context, not the URL param. The cross-tenant superuser carve-out mirrors middleware/rbac.go (CanAccessAllTenants + EnableCrossTenantAccess required). - AddMember no longer records the synthetic "system-<tenantID>" caller (X-API-Key auth path) as invited_by, matching the same filter KB.CreatorID got in PR 2. - AuthHandler now logs a loud one-shot error when constructed with a nil/incomplete config so a wiring bug that silently disables the invite_only gate is visible at boot. Race fixes - New repo helpers DemoteOwnerAtomically / RemoveOwnerAtomically wrap the count-other-owners + write step in a transaction that holds SELECT … FOR UPDATE locks on every other active Owner row. The service routes Owner demotion / Owner removal through them, closing the TOCTOU window where two concurrent demotes of two different Owners could each observe count=2 and both commit, leaving the tenant ownerless. Performance - ListMembers hydration now batches user lookups via a new GetUsersByIDs interface method (one SELECT … WHERE id IN (…) instead of N round-trips). Failure degrades to "no email/username" rather than dropping rows. Frontend - canManage also fires for cross-tenant superusers (authStore.canAccessAllTenants), mirroring the backend carve-out so the UI controls match what the server actually allows. Tests - Cross-tenant URL guard regression cases for List / Add / Update / Remove / Leave. - Superuser bypass tests (flag on -> service reached, flag off -> 403). - Batched lookup test ensures GetUsersByIDs is called exactly once and GetUserByID is never used as a fallback. - Synthetic caller invited_by filter regression test. - Service-layer atomic last-Owner regression tests for both UpdateRole and RemoveMember.