Packages

Usher provides framework-agnostic invitation link management for any Elixir application with Ecto.

Current section

Files

Jump to
usher lib usher.ex
Raw

lib/usher.ex

defmodule Usher do
@moduledoc """
Usher is a web framework-agnostic invitation link management library for
any Elixir application with Ecto.
## Getting Started
To use Usher in your application, you need to:
1. Run the migrations
2. Configure Usher with your repo
3. Use the functions provided to manage invitations
### Database Setup
Generate and run the migration:
mix ecto.gen.migration create_usher_tables
Then add the Usher schema to your migration:
defmodule MyApp.Repo.Migrations.CreateUsherTables do
use Ecto.Migration
def change do
Usher.Migration.create_usher_invitations_table()
end
end
### Configuration
In your `config/config.exs` (or whichever environment you prefer),
set up the Usher configuration:
config :usher,
repo: MyApp.Repo,
token_length: 16,
default_expires_in: {7, :days}
table_name: "myapp_invitations"
All the values above have defaults, which you can find in `Usher.Config`.
### Basic Usage
# Create an invitation
{:ok, invitation} = Usher.create_invitation()
# Get invitation by token
invitation = Usher.get_invitation_by_token("abc123")
# Validate and consume invitation
case Usher.validate_invitation_token("abc123") do
{:ok, invitation} ->
# Proceed with registration
Usher.increment_joined_count(invitation)
{:error, :invalid_token} ->
# Handle invalid token
{:error, :expired} ->
# Handle expired token
end
## Features
- Token generation using cryptographic functions
- Configurable token length and expiration periods
- Framework-agnostic design works with any Ecto-based application
"""
alias Usher.Config
alias Usher.Invitation
alias Usher.Invitations.CreateInvitation
@doc """
Creates a new invitation with a token and default expiration datetime.
## Options
* `:expires_at` - Custom expiration datetime (overrides default)
* `:token` - Custom token (overrides generated token)
## Examples
iex> Usher.create_invitation()
{:ok, %Usher.Invitation{token: "abc123...", expires_at: ~U[...]}}
iex> Usher.create_invitation(expires_at: ~U[2024-12-31 23:59:59Z])
{:ok, %Usher.Invitation{expires_at: ~U[2024-12-31 23:59:59Z]}}
"""
def create_invitation(attrs \\ %{}) do
CreateInvitation.call(attrs)
end
@doc """
Retrieves all invitations.
## Examples
iex> Usher.list_invitations()
[%Usher.Invitation{}, ...]
"""
def list_invitations do
Config.repo().all(Invitation)
end
@doc """
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)
"""
def get_invitation!(id) do
Config.repo().get!(Invitation, id)
end
@doc """
Gets an invitation by token.
## Examples
iex> Usher.get_invitation_by_token("valid_token")
%Usher.Invitation{}
iex> Usher.get_invitation_by_token("invalid")
nil
"""
def get_invitation_by_token(token) when is_binary(token) do
case Config.repo().get_by(Invitation, token: token) do
%Invitation{} = invitation -> {:ok, invitation}
nil -> {:error, :not_found}
end
end
@doc """
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}
"""
def validate_invitation_token(token) do
case get_invitation_by_token(token) do
{:ok, invitation} ->
if DateTime.compare(invitation.expires_at, DateTime.utc_now()) == :gt do
{:ok, invitation}
else
{:error, :invitation_expired}
end
{:error, :not_found} ->
{:error, :invalid_token}
end
end
@doc """
Increments the joined count for an invitation.
This is typically called when a user successfully registers using the invitation.
## Examples
iex> Usher.increment_joined_count(invitation)
{:ok, %Usher.Invitation{joined_count: 1}}
"""
def increment_joined_count(%Invitation{} = invitation) do
invitation
|> Invitation.increment_joined_count_changeset()
|> Config.repo().update()
end
@doc """
Deletes an invitation.
## Examples
iex> Usher.delete_invitation(invitation)
{:ok, %Usher.Invitation{}}
iex> Usher.delete_invitation(bad_invitation)
{:error, %Ecto.Changeset{}}
"""
def delete_invitation(%Invitation{} = invitation) do
Config.repo().delete(invitation)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking invitation changes.
## Examples
iex> Usher.change_invitation(invitation)
%Ecto.Changeset{data: %Usher.Invitation{}}
"""
def change_invitation(%Invitation{} = invitation, attrs \\ %{}) do
Invitation.changeset(invitation, attrs)
end
@doc """
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"
"""
def invitation_url(token, base_url) do
uri = URI.parse(base_url)
query = URI.encode_query([{"invitation_token", token}])
%{uri | query: query} |> URI.to_string()
end
end