Skip to content

admin UI: invite & onboard flow (invite form + public accept page) — frontend for #71 #81

@rrrodzilla

Description

@rrrodzilla

Split from #71. The backend for invite & onboard is complete and merged-pending on feat/issue-71-invite-onboard (email transport, store-not-schema invite table, PASETO invite token, and both endpoints with boot wiring). This issue covers only the generated React admin UI on top of those endpoints.

Backend contract (already built)

POST /api/v1/forge/auth/invites — authenticated. Runs the same privilege guards as POST /users (schema-level CreateUser access, platform_admin-grant guard, Cedar role_rank guard). Emails an accept link, returns the invite reference.

Request:

{ "email": "x@agency.gov", "display_name": "X", "tenant_type": "Organization", "tenant_id": "<ulid>", "role": "member" }

(display_name, tenant_type, tenant_id, role all optional)

Response 201:

{ "invite_id": "<opaque>", "email": "x@agency.gov", "expires_at": "<rfc3339>" }

POST /api/v1/forge/auth/invites/accept — public (no bearer). Looks the invite up by invite_id, reconstructs + re-verifies the stored token (signed claims authoritative), creates the User + TenantMembership, consumes the invite.

Request:

{ "invite_id": "<from emailed link>", "password": "<min 8>", "display_name": "X" }

Response 201:

{ "email": "x@agency.gov", "roles": ["member"] }

The emailed link shape is <public_base_url>/invite/accept?invite=<invite_id>.

Supporting reads already available:

  • GET /api/v1/forge/users/roles — role picker source (name + rank; platform_admin filtered out unless caller holds it).
  • Tenant-root list — schemas annotated @tenant(root) (the "Organization" in a given domain). The org picker should be filtered to tenants the operator can write to.

Frontend work (this issue)

  1. /admin/users/invite — invite form: email, optional display_name, tenant-root picker (from @tenant(root) schemas), role picker (from GET /users/roles). POST to /auth/invites. In dev (email disabled) the endpoint returns a 5xx on send; consider surfacing the invite_id for copy-paste once a dev-mode return path exists (follow-up).
  2. Public /invite/accept?invite=<id> page — unauthenticated route: reads invite from the query string, collects password (+ optional display name), POSTs to /auth/invites/accept, then routes to login on success.
  3. Hide @hidden fields (e.g. password_hash) on the generic create/edit forms — the admin shell should respect the annotation even on the dynamic form.
  4. Registration plumbing: add the new files to ADMIN_FILES + route-manifest, wire into App.tsx routing and the nav.

Out of scope / deferred

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions