feat(invitations): tenant invite CRUD - Mokosh-owned org membership (PMS-244 phase 1) #178

Merged
longjacksonle merged 2 commits from feat/mapps-244-invitations into main 2026-06-11 18:42:29 +02:00

What

Phase 1 of PMS-244 (orgs live in Mokosh, not Bunyip - replaces the closed BUNYIP-95). The additive half: the invitation data model + admin CRUD. Does not touch the login path - phase 2 wires acceptance into the bunyip auth resolution.

How

  • Migration 035 - tenant_invitations (tenant_id FK cascade, email, role, invited_by, status pending|accepted|revoked, 14-day expiry, accepted_at/by). Partial unique index on (tenant_id, lower(email)) WHERE status='pending' (one live invite per tenant+email; re-invite upserts). Second partial index on lower(email) for phase-2's cross-tenant lookup.
  • InvitationsService - create / list_pending / revoke. Emails lowercased; roles validated against the invitable set (all except platform super_admin); sending the first invite from a personal tenant promotes it to org.
  • Routes - POST/GET/DELETE /api/v1/invitations, admin-only, tenant-scoped via u.tenant().

Verification

  • cargo check --all-targets, cargo clippy --all-targets: clean.
  • tests/invitations.rs: create/list/revoke roundtrip, re-invite upsert (no dup), non-invitable role rejected, personal→org promotion - 4 pass.

Next (phase 2, separate PR)

Wire acceptance into auth/middleware.rs::ensure_user_from_bunyip: resolve a pending invite by email → place/re-home the user into that tenant on login; rename ensure_tenant_for_bunyip_orgensure_personal_tenant; remove the dead AtClaims.bunyip_org_id claim path.

🤖 Generated with Claude Code

## What Phase 1 of PMS-244 (orgs live in Mokosh, not Bunyip - replaces the closed BUNYIP-95). The **additive** half: the invitation data model + admin CRUD. **Does not touch the login path** - phase 2 wires acceptance into the bunyip auth resolution. ## How - **Migration 035** - `tenant_invitations` (tenant_id FK cascade, email, role, invited_by, status pending|accepted|revoked, 14-day expiry, accepted_at/by). Partial unique index on `(tenant_id, lower(email)) WHERE status='pending'` (one live invite per tenant+email; re-invite upserts). Second partial index on `lower(email)` for phase-2's cross-tenant lookup. - **`InvitationsService`** - `create` / `list_pending` / `revoke`. Emails lowercased; roles validated against the invitable set (all except platform `super_admin`); sending the first invite from a `personal` tenant promotes it to `org`. - **Routes** - `POST`/`GET`/`DELETE /api/v1/invitations`, admin-only, tenant-scoped via `u.tenant()`. ## Verification - `cargo check --all-targets`, `cargo clippy --all-targets`: clean. - `tests/invitations.rs`: create/list/revoke roundtrip, re-invite upsert (no dup), non-invitable role rejected, personal→org promotion - 4 pass. ## Next (phase 2, separate PR) Wire acceptance into `auth/middleware.rs::ensure_user_from_bunyip`: resolve a pending invite by email → place/re-home the user into that tenant on login; rename `ensure_tenant_for_bunyip_org` → `ensure_personal_tenant`; remove the dead `AtClaims.bunyip_org_id` claim path. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(invitations): tenant invite CRUD - Mokosh-owned org membership (PMS-244, phase 1)
Some checks failed
Check / fmt + clippy + compile + tests (pull_request) Failing after 11s
E2E (staging) / Playwright against staging (pull_request) Successful in 34s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m43s
b54b06cf90
First of two PRs implementing PMS-244 (orgs live in Mokosh, not Bunyip). This is the additive half: the invitation data model + admin CRUD. It does not yet touch the login path - phase 2 wires acceptance into the bunyip auth resolution.

A tenant admin invites a person by email into their tenant. The new `tenant_invitations` table (migration 035) holds pending/accepted/revoked invites with a 14-day expiry and a partial unique index on `(tenant_id, lower(email)) WHERE status = 'pending'` (one live invite per tenant+email; re-inviting upserts role/expiry). A second partial index on `lower(email)` indexes live invites for the cross-tenant lookup phase 2 needs.

`InvitationsService` provides `create` / `list_pending` / `revoke`; emails are lowercased, roles are validated against the invitable set (everything except the platform-level `super_admin`), and sending the first invite from a `personal` tenant promotes it to an `org`. The routes (`POST` / `GET` / `DELETE /api/v1/invitations`) are admin-only and tenant-scoped via `u.tenant()`.

Verification: `cargo check --all-targets` + `cargo clippy --all-targets` clean; `tests/invitations.rs` (create/list/revoke roundtrip, re-invite upsert, non-invitable role rejected, personal->org promotion) - 4 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(auth): resolve SSO login to a tenant via Mokosh invites/self-signup (PMS-244, phase 2)
Some checks failed
Check / fmt + clippy + compile + tests (pull_request) Failing after 14s
E2E (staging) / Playwright against staging (pull_request) Successful in 38s
Create release / Create release from merged PR (pull_request) Has been skipped
Build OCI container / Build and push mokosh-api image (push) Successful in 3m21s
bd8ffd7454
Phase 2 of PMS-244: wire org membership into the bunyip auth path and retire the dead `bunyip_org_id` claim. With this, orgs live entirely in Mokosh.

`ensure_user_from_bunyip` now resolves the user's tenant from Mokosh's own state instead of a token claim, in priority order: a pending invite for the user's verified email wins; else their existing placement; else a brand-new user gets their own `personal` tenant (self-signup). An already-placed user is re-homed into the target on invite acceptance (or the PMS-243 default-tenant backfill) via `rehome_user_between_tenants`; the invite is marked accepted once the user is placed, and a new invited user is seeded with the invite's role. An invite is consumed only for a verified email (gated on userinfo `email_verified`).

Supporting pieces: `AuthService::find_user_tenant(sub)` (tenant by global user id), `InvitationsService::{newest_pending_for, accept}`, and `AuthMiddleware::with_invitations`. The router shares the invitations Arc with both the routes and the middleware.

Reconciles PMS-240/243: `TenantService::ensure_tenant_for_bunyip_org` -> `ensure_personal_tenant(owner_id)` (same provision + `copy_default_config`, keyed on a new `tenants.personal_owner_id`); migration 036 drops `bunyip_org_id` and adds `personal_owner_id` (no FK to users - the tenant is provisioned before the owner's user row exists); `AtClaims.bunyip_org_id` removed. `rehome_user_between_tenants` is kept and reused as the invite-accept mechanism.

Verification: `cargo check --all-targets` + `cargo clippy --all-targets` clean; `tests/invitations.rs` (5, incl. newest-pending lookup -> accept), `tests/tenants.rs` (7, incl. `ensure_personal_tenant` provisioning + re-home), `tests/auth.rs` (14), doctest guard - all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/mapps-244-invitations 2026-06-11 18:42:29 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!178
No description provided.