Invitations
Org owners invite new users by email with optional role pre-assignment. The invited user receives a link, creates an account, and is automatically logged in as a member of the inviting organization with any pre-assigned roles.
Overview
Section titled “Overview”Owner sends invitation (POST /api/invitations/send) → Validates optional role_ids against org roles → Creates invitation record (email, org, unique token, role_ids) → Sends email with accept link: /invite/accept/{token}
Invitee opens link → AcceptInvitationCard loads (POST /api/invitations/details) → Shows pre-filled email + org name
Invitee submits name + password (POST /api/invitations/accept) → Creates: user, email_password_account, member (is_owner: false), session → Assigns pre-selected roles via member_role table → Deletes invitation record → Sets session cookie → auto-login redirect to dashboard
Owner can list pending invitations (GET /api/invitations) → Each invitation shows assigned role namesOwner can revoke a pending invitation (DELETE /api/invitations/{id})Database Schema
Section titled “Database Schema”CREATE TABLE invitation ( id UUID PRIMARY KEY, email TEXT NOT NULL, organization_id UUID NOT NULL REFERENCES organization(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, role_ids JSONB NOT NULL DEFAULT '[]', created_at TIMESTAMP NOT NULL);The role_ids column stores an array of role UUIDs to assign when the invitation is accepted. Defaults to an empty array for invitations without pre-assigned roles.
Invitations have no expiry and no status column — the row is deleted when accepted or revoked.
API Endpoints
Section titled “API Endpoints”Send Invitation
Section titled “Send Invitation”POST /api/invitations/sendRequires: authenticated session with is_owner: true.
Request body:
email: String, // plain string field (Dioxus server function)role_ids: Vec<String>, // optional list of role UUIDs to pre-assignResponse:
pub enum SendInvitationResponse { Success, EmailAlreadyRegistered, // email already has an account InvitationAlreadyExists, // pending invite already sent to this email in this org InvalidEmail, // bad format (no @ or .) InvalidRoleIds, // one or more role IDs don't belong to this org InternalError(String),}What it does:
- Validates
is_owner— only owners can send invitations. - Trims and lowercases the email; validates format.
- Checks for duplicate account and duplicate pending invitation.
- Parses
role_idsto UUIDs and validates all belong to the organization. ReturnsInvalidRoleIdsif any are invalid (all-or-nothing). - Generates a UUID token, inserts the
invitationrow withrole_idsserialized as JSON. - Sends an HTML email via Resend with the accept link:
https://{APP_HOST}/invite/accept/{token}. - If email send fails, rolls back the invitation row so the owner can retry.
Get Invitation Details (unauthenticated)
Section titled “Get Invitation Details (unauthenticated)”POST /api/invitations/detailsNo session required — called by the accept page before the user has an account.
Request body:
token: StringResponse:
pub enum InvitationDetailsResponse { Found(InvitationDetails), NotFound, InternalError(String),}
pub struct InvitationDetails { pub email: String, pub organization_name: String,}Accept Invitation (unauthenticated)
Section titled “Accept Invitation (unauthenticated)”POST /api/invitations/acceptNo session required.
Request body:
token: String,name: String, // display name for the new userpassword: String // minimum 8 charactersResponse:
pub enum AcceptInvitationResponse { Success(String), // contains the session token InvalidToken, // invitation not found InvalidUserName, // empty name PasswordTooShort, // < 8 characters EmailAlreadyRegistered, // race condition guard InternalError(String),}What it does:
- Validates name and password.
- Looks up invitation by token →
InvalidTokenif not found. - Guards against race condition (email registered between send and accept).
- Creates
user,email_password_account(bcrypt password),member(is_owner: false), andsessionrecords. - Deserializes
role_idsfrom the invitation and inserts amember_rolerecord for each role, linking the new member to the pre-assigned roles. - Deletes the invitation row (token is now consumed).
- The
IntoResponseimpl forAcceptInvitationResponse::Successsets the session cookie, triggering auto-login in the browser.
List Pending Invitations
Section titled “List Pending Invitations”GET /api/invitationsRequires: authenticated session with is_owner: true. Returns 403 otherwise.
Response: Vec<PendingInvitation>
pub struct PendingInvitation { pub id: String, // UUID pub email: String, pub created_at: String, // formatted: "YYYY-MM-DD HH:MM" pub roles: Vec<PendingInvitationRole>,}
pub struct PendingInvitationRole { pub id: String, // role UUID pub name: String, // display name}The endpoint batch-fetches role names in a single query across all unique role IDs from pending invitations. Results are ordered by created_at descending.
Revoke Invitation
Section titled “Revoke Invitation”DELETE /api/invitations/{id}Requires: authenticated session with is_owner: true. Returns 403 otherwise.
Deletes the invitation row. The accept link becomes invalid immediately — visiting it shows “Invalid Invitation”.
Session Cookie on Accept
Section titled “Session Cookie on Accept”AcceptInvitationResponse implements axum::IntoResponse to set the cookie:
if let AcceptInvitationResponse::Success(ref session_token) = self { response = response.header("Set-Cookie", session_cookie_set(session_token));}This follows the same pattern as LoginResponse and SignupResponse — the client stores the cookie automatically on a 200 response.
UI Components
Section titled “UI Components”Invite Form (Settings → Organization tab)
Section titled “Invite Form (Settings → Organization tab)”src/mods/settings/components/invite_user_section_component.rs
- Owner-only: the component is not rendered for non-owners.
- Shows an email input + Send Invitation button.
- Includes a collapsible Assign roles picker below the email field.
- Fetches available roles via
get_roles_api()on mount and renders checkboxes. - Tracks selected role IDs in a
Signal<Vec<String>>and passes them tosend_invitation_api(). - The picker toggle shows the selected count (e.g., “Assign roles (2 selected)”).
- Clears selected roles on successful send.
- Fires
on_invitedcallback on success to trigger a list refresh.
Pending Invitations List (Settings → Organization tab)
Section titled “Pending Invitations List (Settings → Organization tab)”src/mods/settings/components/pending_invitations_section_component.rs
- Owner-only section below the invite form.
- Shows email, sent date, and role badges for each pending invitation.
- Role names display as
Badgecomponents withBadgeVariant::Secondary. - Trash icon button calls
DELETE /api/invitations/{id}and removes the row from the list. - Auto-refreshes after a new invitation is sent (via the
on_invitedcallback). - Handles loading, empty, and error states.
Accept Invitation Page
Section titled “Accept Invitation Page”Route: /invite/accept/:token (unauthenticated)
src/shared/views/accept_invitation_view.rs → renders AcceptInvitationCard
src/shared/components/accept_invitation_card_component.rs
- On mount: calls
POST /api/invitations/detailswith the token. - Shows org name and pre-fills the email (read-only).
- User fills in name + password and submits.
- On
Success: stores the session token and redirects to the dashboard. - On
InvalidToken: shows “Invalid Invitation” card.
Module Structure
Section titled “Module Structure”src/mods/invitation/├── api/│ ├── send_invitation_api.rs # POST /api/invitations/send│ ├── get_invitation_details_api.rs # POST /api/invitations/details│ ├── accept_invitation_api.rs # POST /api/invitations/accept│ ├── get_invitations_api.rs # GET /api/invitations│ └── revoke_invitation_api.rs # DELETE /api/invitations/{id}└── mod.rs
src/bases/auth/types/└── accept_invitation_response_type.rs # IntoResponse impl (sets session cookie)
src/shared/├── types/│ └── invitation_response_type.rs # all response + data types├── views/│ └── accept_invitation_view.rs # /invite/accept/:token page└── components/ └── accept_invitation_card_component.rs
src/mods/settings/components/├── invite_user_section_component.rs # send form└── pending_invitations_section_component.rs # list + revokeRelated Pages
Section titled “Related Pages”- ABAC Authorization — how roles and permissions work
- Settings — the Organization tab hosts the invite UI