Organizations with Invitations

Invite multiple users to an organization at once with bearer invitation links. From the link, invited users will be prompted to register and accept their invitation. Includes invitation management with expiring invitations.

Level: Medium
11
477
2

Email notifications

A new deliver_organization_invitation/2 function sends invitation emails. The email includes the organization name and a link to accept the invitation.

Organizations context additions

The Organizations context is extended with invitation management functions:

The send_invitation/3 function uses a transaction to build the invitation, insert it, and send the email atomically. If any step fails, the entire operation is rolled back.

The accept_invitation/3 function deletes the invitation and adds the user as a member in a single transaction. It accepts a URL function to generate the organization URL for the welcome notification email.

The Invitation schema

The Invitation schema stores pending invitations. Each invitation has an email , a hashed token , and belongs to an organization. We only store inserted_at (no updated_at ) since invitations are immutable once created.

The build/2 function generates a cryptographically secure random token, hashes it with SHA256, and returns both the URL-safe encoded token (for the invitation link) and the invitation struct (with the hashed token for storage). This pattern is similar to how password reset tokens work.

Invitations expire after a configurable number of days (default 3). The verify_token_query/1 function decodes and hashes the provided token, then builds a query that only matches non-expired invitations. The expired?/1 and expiration/1 helper functions support displaying expiration status in the UI.

Invitation controller

The InvitationController handles the invitation acceptance flow with two actions.

The show/2 action displays the invitation acceptance page. It handles several cases:

The accept/2 action processes the form submission, accepting the invitation and redirecting to the organization.

Invitation acceptance template

The acceptance page shows the organization name and a simple form with a hidden token field and an accept button.

Dynamic form components

Two components support the dynamic form fields:

drop_field_wrapper/1 wraps each email input with a remove button (hidden checkbox). When checked, the field is removed from the form on the next validation.

add_field_button/1 renders a button (hidden checkbox) that adds a new empty field when clicked.

These components use Ecto's sort_param and drop_param feature to manage dynamic embedded fields without writing JavaScript.

Members index updates

The members index page is updated to show pending invitations alongside current members. A second table displays all invitations with their email and expiration status.

Expired invitations are highlighted in red. Owners can delete invitations directly from the list.

The "Add Member" button is replaced with "Invite Members" to reflect the new flow.

Invite members LiveView

The Invite LiveView allows owners to send invitations to multiple email addresses at once. It uses an embedded schema form with dynamic fields that can be added or removed.

The inner Form module defines an embedded schema with a list of invitation embeds. It uses Ecto's sort_param and drop_param options to enable dynamic field management without JavaScript.

The form starts with 3 empty email fields. Users can add more fields or remove existing ones using the add/remove buttons. When submitted, the form collects all non-empty emails and sends invitations to each.

Router changes

Three new routes are added:

The show route is public so that users who aren't logged in can be redirected to register first, with the invitation URL stored in their session.

Database migration

The migration creates the organizations_invitations table with:

The token has a unique index to ensure fast lookups and prevent duplicate tokens.