Usher (Usher v0.5.1)
View SourceUsher is a web framework-agnostic invitation link management library for any Elixir application with Ecto.
Summary
Functions
Returns an %Ecto.Changeset{} for tracking invitation changes.
Creates a new invitation with a token and default expiration datetime.
Creates an invitation and returns a signed presentation token alongside it.
Deletes an invitation.
Checks if a specific entity has performed an action on an invitation.
Extends the expiration of an invitation by the given duration.
Gets a single invitation by ID. Raises if not found.
Gets an invitation by token.
Builds an invitation URL for the given token and base URL.
Gets all usage records for an invitation.
Gets all usage records for an invitiation, grouped by unique entity IDs.
Retrieves all invitations.
Removes the expiration from an invitation, making it never expire.
Sets a specific expiration date for an invitation.
Builds an invitation URL for the given token and signature using the base URL.
Records an entity's usage of an invitation.
Validates an invitation token exists and returns the invitation if valid.
Validates the invitation token against the given signature and returns the invitation, if the signature is valid.
Types
Functions
Returns an %Ecto.Changeset{} for tracking invitation changes.
Options
:require_name- Whether to require the name field (defaults to false)
Examples
iex> Usher.change_invitation(invitation)
%Ecto.Changeset{data: %Usher.Invitation{}}
iex> Usher.change_invitation(invitation, %{name: "Test"})
%Ecto.Changeset{data: %Usher.Invitation{}}
iex> Usher.change_invitation(invitation, %{}, require_name: true)
%Ecto.Changeset{data: %Usher.Invitation{}, errors: [name: {"can't be blank", _}]}
Creates a new invitation with a token and default expiration datetime.
Attributes
:name- Name for the invitation:expires_at- Custom expiration datetime (overrides default):token- Custom token (overrides generated token)
Options
:require_name- Whether to require the name field (defaults to false)
Examples
iex> Usher.create_invitation()
{:ok, %Usher.Invitation{token: "abc123...", expires_at: ~U[...]}}
iex> Usher.create_invitation(%{name: "Welcome Team"})
{:ok, %Usher.Invitation{name: "Welcome Team"}}
iex> Usher.create_invitation(%{}, require_name: true)
{:error, %Ecto.Changeset{errors: [name: {"can't be blank", _}]}}
@spec create_invitation_with_signed_token( map(), keyword() ) :: {:ok, Usher.Invitation.t(), signed_token()} | {:error, Ecto.Changeset.t() | :token_required}
Creates an invitation and returns a signed presentation token alongside it.
This is useful for scenarios where you want to use a user-friendly token, but want to ensure authenticity, as user-friendly tokens are more likely to be guessed or fabricated.
Only works when a :token is supplied in the attrs. If you're not supplying a
token, or do not care about the authenticity of invitation tokens, use
Usher.create_invitation/2 instead.
Deletes an invitation.
Examples
iex> Usher.delete_invitation(invitation)
{:ok, %Usher.Invitation{}}
iex> Usher.delete_invitation(bad_invitation)
{:error, %Ecto.Changeset{}}
Checks if a specific entity has performed an action on an invitation.
Examples
# Check if user 123 has registered
if Usher.entity_used_invitation?(invitation, "user", "123", "registered") do
# Entity has already registered
end
# Check if entity has any usage
if Usher.entity_used_invitation?(invitation, "user", "123") do
# Entity has used this invitation for any action
end
@spec extend_invitation_expiration( Usher.Invitation.t(), {pos_integer(), Usher.Config.duration_unit_pair()} ) :: {:ok, Usher.Invitation.t()} | {:error, Ecto.Changeset.t() | :no_expiration_to_extend}
Extends the expiration of an invitation by the given duration.
Only works with invitations that already have an expiration date. Returns an error if the invitation has no expiration date (nil).
Parameters
invitation- The invitation struct to extendduration- A tuple like{7, :day}or{2, :hour}
Examples
# Extend an invitation by 7 days
iex> Usher.extend_invitation_expiration(invitation, {7, :day})
{:ok, %Usher.Invitation{expires_at: ~U[...]}}
# Extend an expired invitation by 2 hours
iex> Usher.extend_invitation_expiration(expired_invitation, {2, :hour})
{:ok, %Usher.Invitation{expires_at: ~U[...]}}
# Try to extend a never-expiring invitation
iex> Usher.extend_invitation_expiration(never_expiring_invitation, {1, :week})
{:error, :no_expiration_to_extend}
Gets a single invitation by ID. Raises if not found.
Examples
iex> Usher.get_invitation!(id)
%Usher.Invitation{}
iex> Usher.get_invitation!("nonexistent")
** (Ecto.NoResultsError)
Gets an invitation by token.
Examples
iex> Usher.get_invitation_by_token("valid_token")
%Usher.Invitation{}
iex> Usher.get_invitation_by_token("invalid")
nil
Builds an invitation URL for the given token and base URL.
Examples
iex> Usher.invitation_url("abc123", "https://example.com/signup")
"https://example.com/signup?invitation_token=abc123"
@spec list_invitation_usages( Usher.Invitation.t(), keyword() ) :: [Usher.InvitationUsage.t()]
Gets all usage records for an invitation.
Options
:entity_type- Filter by entity type:entity_id- Filter by entity ID:action- Filter by action:limit- Limit number of results
Examples
# Get all usages for an invitation
usages = Usher.list_invitation_usages(invitation)
# Get only user registrations
usages = Usher.list_invitation_usages(invitation, entity_type: :user, action: :registered)
@spec list_invitation_usages_by_unique_entity( Usher.Invitation.t(), keyword() ) :: [{String.t(), [invitation_usages_by_unique_entity()]}]
Gets all usage records for an invitiation, grouped by unique entity IDs.
Options
:entity_type- Filter by entity type, useful for getting unique usages of a specific entity type:entity_id- Filter by entity ID, useful for getting unique usages of a specific entity:action- Filter by action, useful for getting unique usages of a specific action:limit- Limit number of results
Examples
# All unique entities that used the invitation
unique_entities = Usher.list_invitation_usages_by_unique_entity(invitation)
# All entities of a specific type that used the invitation
unique_users = Usher.list_invitation_usages_by_unique_entity(invitation, entity_type: :user)
# All entities that took a specific action with the invitation
unique_registrations = Usher.list_invitation_usages_by_unique_entity(
invitation,
action: :registered
)
# A specific entity and the actions they took with the invitation
unique_entity_actions = Usher.list_invitation_usages_by_unique_entity(
invitation,
entity_id: "123"
)
Retrieves all invitations.
Examples
iex> Usher.list_invitations()
[%Usher.Invitation{}, ...]
@spec remove_invitation_expiration(Usher.Invitation.t()) :: {:ok, Usher.Invitation.t()} | {:error, Ecto.Changeset.t()}
Removes the expiration from an invitation, making it never expire.
Sets the expires_at field to nil, effectively creating a permanent invitation link.
Parameters
invitation- The invitation struct to update
Examples
# Make an invitation never expire
iex> Usher.remove_invitation_expiration(invitation)
{:ok, %Usher.Invitation{expires_at: nil}}
# Remove expiration from an already expired invitation
iex> Usher.remove_invitation_expiration(expired_invitation)
{:ok, %Usher.Invitation{expires_at: nil}}
@spec set_invitation_expiration(Usher.Invitation.t(), DateTime.t()) :: {:ok, Usher.Invitation.t()} | {:error, Ecto.Changeset.t()}
Sets a specific expiration date for an invitation.
Works with any invitation, regardless of current expiration state.
Parameters
invitation- The invitation struct to updateexpires_at- ADateTimestruct for the new expiration date
Examples
# Set a specific expiration date
iex> Usher.set_invitation_expiration(invitation, ~U[2025-12-31 23:59:59Z])
{:ok, %Usher.Invitation{expires_at: ~U[2025-12-31 23:59:59Z]}}
# Set expiration to 30 days from now
iex> future_date = DateTime.add(DateTime.utc_now(), 30, :day)
iex> Usher.set_invitation_expiration(invitation, future_date)
{:ok, %Usher.Invitation{expires_at: future_date}}
@spec signed_invitation_url( Usher.Token.Signature.token(), Usher.Token.Signature.signature(), String.t() ) :: String.t()
Builds an invitation URL for the given token and signature using the base URL.
Examples
iex> Usher.signed_invitation_url("abc123", "Z7aPPn0OT3ARmifwmGJkMRec74H1AV-RwtpUqN8Ev2c", "https://example.com/signup")
"https://example.com/signup?invitation_token=abc123&s=Z7aPPn0OT3ARmifwmGJkMRec74H1AV-RwtpUqN8Ev2c"
@spec track_invitation_usage( Usher.Invitation.t() | String.t(), atom(), String.t(), atom(), map() ) :: {:ok, Usher.InvitationUsage.t()} | {:error, Ecto.Changeset.t()} | {:error, :invitation_not_found}
Records an entity's usage of an invitation.
This provides flexible tracking of how invitations are used. You can track different actions (like :visited, :registered, :activated) and different entity types (like :user, :company, :device).
Parameters
invitation_or_token- An%Invitation{}struct or invitation token stringentity_type- String describing the type of entity (e.g., :user, :company, :device)entity_id- String ID of the entityaction- String describing the action (e.g., :visited, :registered, :activated)metadata- Optional map of additional data (e.g., user agent, IP, custom fields)
Examples
# Track a user visiting signup page
{:ok, usage} = Usher.track_invitation_usage(
"abc123",
:user,
"user_123",
:visited,
%{ip: "192.168.1.1", user_agent: "Mozilla/5.0..."}
)
# Track a company registration
{:ok, usage} = Usher.track_invitation_usage(
invitation,
:company,
"company_456",
:registered,
%{plan: "premium", source: "email_campaign"}
)
# Tracking without metadata
{:ok, usage} = Usher.track_invitation_usage("abc123", :user, "789", :activated)
Validates an invitation token exists and returns the invitation if valid.
Returns {:ok, invitation} if the token exists and hasn't expired.
Returns {:error, reason} if the token is invalid or expired.
Examples
iex> Usher.validate_invitation_token("valid_token")
{:ok, %Usher.Invitation{}}
iex> Usher.validate_invitation_token("expired_token")
{:error, :invitation_expired}
iex> Usher.validate_invitation_token("invalid_token")
{:error, :invalid_token}
@spec validate_secure_invitation_token( Usher.Token.Signature.token(), Usher.Token.Signature.signature() ) :: {:ok, Usher.Invitation.t()} | {:error, Ecto.Changeset.t() | :invalid_signature}
Validates the invitation token against the given signature and returns the invitation, if the signature is valid.
Returns {:error, invalid_signature} if the signature is invalid.
Examples
iex> Usher.validate_secure_invitation_token("valid_token", "S9cjQ8oET4qrHZjwdgbNsc9H3wwIn_e1st9E5A2GmXA")
{:ok, %Usher.Invitation{}}
iex> Usher.validate_secure_invitation_token("valid_token", "invalid-signature")
{:error, :invalid_signature}
iex> Usher.validate_invitation_token("expired_token", "S9cjQ8oET4qrHZjwdgbNsc9H3wwIn_e1st9E5A2GmXA")
{:error, :invitation_expired}