navree
Documentation

Invitations

Invite people by email, accept by token, revoke and expire invitations.

On this page

Invitations bring people into a workspace who may not have an account yet. An admin invites an email address with a role; Navree mails a link carrying a single-use token; the invitee signs in (or signs up) and accepts, and joins the workspace with that role.

The flow

  1. An admin calls POST /api/workspaces/{workspace}/invitations with {"email", "role"}.
  2. Navree stores the invitation with a SHA-256 digest of a random 48-character token, and once the request commits, mails the token to the address.
  3. The invitee opens the link in your front end, signs in or registers, and the front end posts the token to POST /api/invitations/accept.
  4. Navree checks the token, the expiry and the address, adds the member and marks the invitation accepted.

Sending an invitation

Over HTTP, with the workspace.invitations.create permission (owners and admins have it):

HTTP
201 Created

role defaults to member. The response never contains the token: it travels in the mail only.

In code, Invitations::invite returns the invitation and the plain token, for apps that deliver it another way (its Debug output hides the token):

Rust

The rules:

  • The address is trimmed and lower-cased, and must look like an email address (a 400 on email otherwise).
  • The role is any built-in or configured role except owner (a 400 on role). To make someone the owner, invite them as admin, then transfer ownership.
  • An address that belongs to a member already is a 409, The user is already a member of this workspace.
  • Inviting an address again replaces its pending invitation: the old token stops working.

The mail

With MailModule in the app, the mail goes out once the request commits, through Mailer::queue (on a queue worker when the app has a queue). Without a mailer the invitation is created, nothing is mailed, and a warning is logged.

Set the page of your front end that accepts invitations, and the mail links to it with the token as a query parameter:

config/app.toml
The built-in mail

Without url, the mail carries the token itself, for API and mobile clients. The inviter's name comes from the user directory.

To write your own mail, point mail_template at a view pair ({template}.html and {template}.txt):

config/app.toml
resources/views/mail/workspace-invitation.txt

The template receives workspace (the name), email, role, inviter (empty when unknown), token, url (empty without url) and expires_in_days. The subject stays You're invited to join <workspace>.

Accepting

The invitee must be signed in. Your front end posts the token from the link:

HTTP
200 OK
SituationStatusmessage
Unknown, revoked or already used token404Invitation not found.
The invitation has expired410The invitation has expired.
Signed in as another address403This invitation was sent to a different email address.
Already a member409The user is already a member of this workspace.
More than 10 attempts a minute429Too Many Requests

Someone without an account signs up first (POST /api/auth/register with the invited address), then accepts with the same token. Keep the token in the front end across the sign-up.

In code:

Rust

Matching the address

With workspace.invitations.require_matching_email (the default), only the account whose email is the invited address can accept. Navree asks the user directory for the accepting user's address. When the directory doesn't know the user (no users table, or users_table = ""), the check can't pass and acceptance is refused with the 403 above: register a user directory for your user store, or turn the check off to treat the token alone as proof:

config/app.toml

Revoking

HTTP

Revoking (permission workspace.invitations.delete) deletes the pending invitation, and its token stops working. GET /api/workspaces/{workspace}/invitations (permission workspace.invitations.read) lists the pending ones, newest first. In code: invitations.revoke(&uow, workspace_id, invitation_id) and invitations.pending(workspace_id).

Expiry

An invitation can be accepted for workspace.invitations.ttl_secs seconds, seven days by default. Expired invitations drop out of the pending list and find_by_token, and accepting one is a 410. The rows stay until you prune them:

Shell

Schedule it daily:

Rust

Showing an invitation before accepting

invitations.find_by_token(&token) returns the pending invitation (with its workspace_id, email and role), or None for an unknown, used or expired token. Use it in your own route to show "Ada invited you to Acme" before the user accepts.

Events

EventNamePayload
InvitationSentworkspace.invitation_sentworkspace_id, invitation_id, email, role, invited_by, expires_at (never the token)
InvitationAcceptedworkspace.invitation_acceptedworkspace_id, invitation_id, user_id, role

Accepting also emits MemberAdded.

Testing

Read the token from the mail the array transport kept, and move time with a fixed clock:

Rust

Register the clock with override_value::<dyn Clock>(clock.clone()) where clock is an Arc<FixedClock>, and set mail.transport = "array".