forked from shibao/cannery
		
	add invites
This commit is contained in:
		@@ -4,8 +4,9 @@ defmodule Lokal.Accounts do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  import Ecto.Query, warn: false
 | 
			
		||||
  alias Lokal.Repo
 | 
			
		||||
  alias Lokal.Accounts.{User, UserNotifier, UserToken}
 | 
			
		||||
  alias Lokal.{Mailer, Repo}
 | 
			
		||||
  alias Lokal.Accounts.{User, UserToken}
 | 
			
		||||
  alias Ecto.{Changeset, Multi}
 | 
			
		||||
 | 
			
		||||
  ## Database getters
 | 
			
		||||
 | 
			
		||||
@@ -21,9 +22,8 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      nil
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def get_user_by_email(email) when is_binary(email) do
 | 
			
		||||
    Repo.get_by(User, email: email)
 | 
			
		||||
  end
 | 
			
		||||
  @spec get_user_by_email(String.t()) :: User.t() | nil
 | 
			
		||||
  def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email)
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Gets a user by email and password.
 | 
			
		||||
@@ -37,6 +37,8 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      nil
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec get_user_by_email_and_password(String.t(), String.t()) ::
 | 
			
		||||
          User.t() | nil
 | 
			
		||||
  def get_user_by_email_and_password(email, password)
 | 
			
		||||
      when is_binary(email) and is_binary(password) do
 | 
			
		||||
    user = Repo.get_by(User, email: email)
 | 
			
		||||
@@ -57,8 +59,38 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      ** (Ecto.NoResultsError)
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec get_user!(User.t()) :: User.t()
 | 
			
		||||
  def get_user!(id), do: Repo.get!(User, id)
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns all users grouped by role.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> list_users_by_role(%User{id: 123, role: :admin})
 | 
			
		||||
      [admin: [%User{}], user: [%User{}, %User{}]]
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec list_all_users_by_role(User.t()) :: %{String.t() => [User.t()]}
 | 
			
		||||
  def list_all_users_by_role(%User{role: :admin}) do
 | 
			
		||||
    Repo.all(from u in User, order_by: u.email) |> Enum.group_by(fn user -> user.role end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns all users for a certain role.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> list_users_by_role(%User{id: 123, role: :admin})
 | 
			
		||||
      [%User{}]
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec list_users_by_role(:admin | :user) :: [User.t()]
 | 
			
		||||
  def list_users_by_role(role) do
 | 
			
		||||
    role = role |> to_string()
 | 
			
		||||
    Repo.all(from u in User, where: u.role == ^role, order_by: u.email)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  ## User registration
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
@@ -70,42 +102,61 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      {:ok, %User{}}
 | 
			
		||||
 | 
			
		||||
      iex> register_user(%{field: bad_value})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
      {:error, %Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec register_user(map()) :: {:ok, User.t()} | {:error, Changeset.t(User.new_user())}
 | 
			
		||||
  def register_user(attrs) do
 | 
			
		||||
    %User{}
 | 
			
		||||
    |> User.registration_changeset(attrs)
 | 
			
		||||
    |> Repo.insert()
 | 
			
		||||
    # if no registered users, make first user an admin
 | 
			
		||||
    role =
 | 
			
		||||
      if Repo.one!(from u in User, select: count(u.id), distinct: true) == 0,
 | 
			
		||||
        do: "admin",
 | 
			
		||||
        else: "user"
 | 
			
		||||
 | 
			
		||||
    %User{} |> User.registration_changeset(attrs |> Map.put("role", role)) |> Repo.insert()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an `%Ecto.Changeset{}` for tracking user changes.
 | 
			
		||||
  Returns an `%Changeset{}` for tracking user changes.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> change_user_registration(user)
 | 
			
		||||
      %Ecto.Changeset{data: %User{}}
 | 
			
		||||
      %Changeset{data: %User{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def change_user_registration(%User{} = user, attrs \\ %{}) do
 | 
			
		||||
    User.registration_changeset(user, attrs, hash_password: false)
 | 
			
		||||
  end
 | 
			
		||||
  @spec change_user_registration(User.t() | User.new_user()) ::
 | 
			
		||||
          Changeset.t(User.t() | User.new_user())
 | 
			
		||||
  @spec change_user_registration(User.t() | User.new_user(), map()) ::
 | 
			
		||||
          Changeset.t(User.t() | User.new_user())
 | 
			
		||||
  def change_user_registration(user, attrs \\ %{}),
 | 
			
		||||
    do: User.registration_changeset(user, attrs, hash_password: false)
 | 
			
		||||
 | 
			
		||||
  ## Settings
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an `%Ecto.Changeset{}` for changing the user email.
 | 
			
		||||
  Returns an `%Changeset{}` for changing the user email.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> change_user_email(user)
 | 
			
		||||
      %Ecto.Changeset{data: %User{}}
 | 
			
		||||
      %Changeset{data: %User{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def change_user_email(user, attrs \\ %{}) do
 | 
			
		||||
    User.email_changeset(user, attrs)
 | 
			
		||||
  end
 | 
			
		||||
  @spec change_user_email(User.t(), map()) :: Changeset.t(User.t())
 | 
			
		||||
  def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs)
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an `%Changeset{}` for changing the user role.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> change_user_role(user)
 | 
			
		||||
      %Changeset{data: %User{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec change_user_role(User.t(), atom()) :: Changeset.t(User.t())
 | 
			
		||||
  def change_user_role(user, role), do: User.role_changeset(user, role)
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Emulates that the email will change without actually changing
 | 
			
		||||
@@ -117,14 +168,16 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      {:ok, %User{}}
 | 
			
		||||
 | 
			
		||||
      iex> apply_user_email(user, "invalid password", %{email: ...})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
      {:error, %Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec apply_user_email(User.t(), String.t(), map()) ::
 | 
			
		||||
          {:ok, User.t()} | {:error, Changeset.t(User.t())}
 | 
			
		||||
  def apply_user_email(user, password, attrs) do
 | 
			
		||||
    user
 | 
			
		||||
    |> User.email_changeset(attrs)
 | 
			
		||||
    |> User.validate_current_password(password)
 | 
			
		||||
    |> Ecto.Changeset.apply_action(:update)
 | 
			
		||||
    |> Changeset.apply_action(:update)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
@@ -133,6 +186,7 @@ defmodule Lokal.Accounts do
 | 
			
		||||
  If the token matches, the user email is updated and the token is deleted.
 | 
			
		||||
  The confirmed_at date is also updated to the current time.
 | 
			
		||||
  """
 | 
			
		||||
  @spec update_user_email(User.t(), String.t()) :: :ok | :error
 | 
			
		||||
  def update_user_email(user, token) do
 | 
			
		||||
    context = "change:#{user.email}"
 | 
			
		||||
 | 
			
		||||
@@ -145,12 +199,13 @@ defmodule Lokal.Accounts do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec user_email_multi(User.t(), String.t(), String.t()) :: Multi.t()
 | 
			
		||||
  defp user_email_multi(user, email, context) do
 | 
			
		||||
    changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset()
 | 
			
		||||
 | 
			
		||||
    Ecto.Multi.new()
 | 
			
		||||
    |> Ecto.Multi.update(:user, changeset)
 | 
			
		||||
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context]))
 | 
			
		||||
    Multi.new()
 | 
			
		||||
    |> Multi.update(:user, changeset)
 | 
			
		||||
    |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context]))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
@@ -162,26 +217,26 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      {:ok, %{to: ..., body: ...}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def deliver_update_email_instructions(%User{} = user, current_email, update_email_url_fun)
 | 
			
		||||
  @spec deliver_update_email_instructions(User.t(), String.t(), function) :: Job.t()
 | 
			
		||||
  def deliver_update_email_instructions(user, current_email, update_email_url_fun)
 | 
			
		||||
      when is_function(update_email_url_fun, 1) do
 | 
			
		||||
    {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
 | 
			
		||||
 | 
			
		||||
    Repo.insert!(user_token)
 | 
			
		||||
    UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
 | 
			
		||||
    Mailer.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an `%Ecto.Changeset{}` for changing the user password.
 | 
			
		||||
  Returns an `%Changeset{}` for changing the user password.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> change_user_password(user)
 | 
			
		||||
      %Ecto.Changeset{data: %User{}}
 | 
			
		||||
      %Changeset{data: %User{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def change_user_password(user, attrs \\ %{}) do
 | 
			
		||||
    User.password_changeset(user, attrs, hash_password: false)
 | 
			
		||||
  end
 | 
			
		||||
  @spec change_user_password(User.t(), map()) :: Changeset.t(User.t())
 | 
			
		||||
  def change_user_password(user, attrs \\ %{}),
 | 
			
		||||
    do: User.password_changeset(user, attrs, hash_password: false)
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Updates the user password.
 | 
			
		||||
@@ -192,18 +247,20 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      {:ok, %User{}}
 | 
			
		||||
 | 
			
		||||
      iex> update_user_password(user, "invalid password", %{password: ...})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
      {:error, %Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec update_user_password(User.t(), String.t(), map()) ::
 | 
			
		||||
          {:ok, User.t()} | {:error, Changeset.t(User.t())}
 | 
			
		||||
  def update_user_password(user, password, attrs) do
 | 
			
		||||
    changeset =
 | 
			
		||||
      user
 | 
			
		||||
      |> User.password_changeset(attrs)
 | 
			
		||||
      |> User.validate_current_password(password)
 | 
			
		||||
 | 
			
		||||
    Ecto.Multi.new()
 | 
			
		||||
    |> Ecto.Multi.update(:user, changeset)
 | 
			
		||||
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
 | 
			
		||||
    Multi.new()
 | 
			
		||||
    |> Multi.update(:user, changeset)
 | 
			
		||||
    |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
 | 
			
		||||
    |> Repo.transaction()
 | 
			
		||||
    |> case do
 | 
			
		||||
      {:ok, %{user: user}} -> {:ok, user}
 | 
			
		||||
@@ -211,11 +268,28 @@ defmodule Lokal.Accounts do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes a user. must be performed by an admin or the same user!
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> delete_user!(user_to_delete, %User{id: 123, role: :admin})
 | 
			
		||||
      %User{}
 | 
			
		||||
 | 
			
		||||
      iex> delete_user!(%User{id: 123}, %User{id: 123})
 | 
			
		||||
      %User{}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec delete_user!(User.t(), User.t()) :: User.t()
 | 
			
		||||
  def delete_user!(user, %User{role: :admin}), do: user |> Repo.delete!()
 | 
			
		||||
  def delete_user!(%User{id: user_id} = user, %User{id: user_id}), do: user |> Repo.delete!()
 | 
			
		||||
 | 
			
		||||
  ## Session
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Generates a session token.
 | 
			
		||||
  """
 | 
			
		||||
  @spec generate_user_session_token(User.t()) :: String.t()
 | 
			
		||||
  def generate_user_session_token(user) do
 | 
			
		||||
    {token, user_token} = UserToken.build_session_token(user)
 | 
			
		||||
    Repo.insert!(user_token)
 | 
			
		||||
@@ -225,6 +299,7 @@ defmodule Lokal.Accounts do
 | 
			
		||||
  @doc """
 | 
			
		||||
  Gets the user with the given signed token.
 | 
			
		||||
  """
 | 
			
		||||
  @spec get_user_by_session_token(String.t()) :: User.t()
 | 
			
		||||
  def get_user_by_session_token(token) do
 | 
			
		||||
    {:ok, query} = UserToken.verify_session_token_query(token)
 | 
			
		||||
    Repo.one(query)
 | 
			
		||||
@@ -233,11 +308,30 @@ defmodule Lokal.Accounts do
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes the signed token with the given context.
 | 
			
		||||
  """
 | 
			
		||||
  @spec delete_session_token(String.t()) :: :ok
 | 
			
		||||
  def delete_session_token(token) do
 | 
			
		||||
    Repo.delete_all(UserToken.token_and_context_query(token, "session"))
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns a boolean if registration is allowed or not
 | 
			
		||||
  """
 | 
			
		||||
  @spec allow_registration?() :: boolean()
 | 
			
		||||
  def allow_registration? do
 | 
			
		||||
    Application.get_env(:lokal, LokalWeb.Endpoint)[:registration] == "public" or
 | 
			
		||||
      list_users_by_role(:admin) |> Enum.empty?()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Checks if user is an admin
 | 
			
		||||
  """
 | 
			
		||||
  @spec is_admin?(User.t()) :: boolean()
 | 
			
		||||
  def is_admin?(%User{id: user_id}) do
 | 
			
		||||
    Repo.one(from u in User, where: u.id == ^user_id and u.role == :admin)
 | 
			
		||||
    |> is_nil()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  ## Confirmation
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
@@ -252,14 +346,15 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      {:error, :already_confirmed}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun)
 | 
			
		||||
  @spec deliver_user_confirmation_instructions(User.t(), function) :: Job.t()
 | 
			
		||||
  def deliver_user_confirmation_instructions(user, confirmation_url_fun)
 | 
			
		||||
      when is_function(confirmation_url_fun, 1) do
 | 
			
		||||
    if user.confirmed_at do
 | 
			
		||||
      {:error, :already_confirmed}
 | 
			
		||||
    else
 | 
			
		||||
      {encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
 | 
			
		||||
      Repo.insert!(user_token)
 | 
			
		||||
      UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
 | 
			
		||||
      Mailer.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -269,6 +364,7 @@ defmodule Lokal.Accounts do
 | 
			
		||||
  If the token matches, the user account is marked as confirmed
 | 
			
		||||
  and the token is deleted.
 | 
			
		||||
  """
 | 
			
		||||
  @spec confirm_user(String.t()) :: {:ok, User.t()} | atom()
 | 
			
		||||
  def confirm_user(token) do
 | 
			
		||||
    with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
 | 
			
		||||
         %User{} = user <- Repo.one(query),
 | 
			
		||||
@@ -279,10 +375,11 @@ defmodule Lokal.Accounts do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec confirm_user_multi(User.t()) :: Multi.t()
 | 
			
		||||
  def confirm_user_multi(user) do
 | 
			
		||||
    Ecto.Multi.new()
 | 
			
		||||
    |> Ecto.Multi.update(:user, User.confirm_changeset(user))
 | 
			
		||||
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"]))
 | 
			
		||||
    Multi.new()
 | 
			
		||||
    |> Multi.update(:user, User.confirm_changeset(user))
 | 
			
		||||
    |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"]))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  ## Reset password
 | 
			
		||||
@@ -296,11 +393,12 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      {:ok, %{to: ..., body: ...}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun)
 | 
			
		||||
  @spec deliver_user_reset_password_instructions(User.t(), function()) :: Job.t()
 | 
			
		||||
  def deliver_user_reset_password_instructions(user, reset_password_url_fun)
 | 
			
		||||
      when is_function(reset_password_url_fun, 1) do
 | 
			
		||||
    {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password")
 | 
			
		||||
    Repo.insert!(user_token)
 | 
			
		||||
    UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
 | 
			
		||||
    Mailer.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
@@ -315,6 +413,7 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      nil
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec get_user_by_reset_password_token(String.t()) :: User.t() | nil
 | 
			
		||||
  def get_user_by_reset_password_token(token) do
 | 
			
		||||
    with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
 | 
			
		||||
         %User{} = user <- Repo.one(query) do
 | 
			
		||||
@@ -333,13 +432,14 @@ defmodule Lokal.Accounts do
 | 
			
		||||
      {:ok, %User{}}
 | 
			
		||||
 | 
			
		||||
      iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
 | 
			
		||||
      {:error, %Ecto.Changeset{}}
 | 
			
		||||
      {:error, %Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec reset_user_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t(User.t())}
 | 
			
		||||
  def reset_user_password(user, attrs) do
 | 
			
		||||
    Ecto.Multi.new()
 | 
			
		||||
    |> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
 | 
			
		||||
    |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
 | 
			
		||||
    Multi.new()
 | 
			
		||||
    |> Multi.update(:user, User.password_changeset(user, attrs))
 | 
			
		||||
    |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
 | 
			
		||||
    |> Repo.transaction()
 | 
			
		||||
    |> case do
 | 
			
		||||
      {:ok, %{user: user}} -> {:ok, user}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										173
									
								
								lib/lokal/invites.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								lib/lokal/invites.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
defmodule Lokal.Invites do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  The Invites context.
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  import Ecto.Query, warn: false
 | 
			
		||||
  alias Lokal.{Accounts.User, Invites.Invite, Repo}
 | 
			
		||||
  alias Ecto.Changeset
 | 
			
		||||
 | 
			
		||||
  @invite_token_length 20
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns the list of invites.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> list_invites(%User{id: 123, role: :admin})
 | 
			
		||||
      [%Invite{}, ...]
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec list_invites(User.t()) :: [Invite.t()]
 | 
			
		||||
  def list_invites(%User{role: :admin}) do
 | 
			
		||||
    Repo.all(from i in Invite, order_by: i.name)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Gets a single invite.
 | 
			
		||||
 | 
			
		||||
  Raises `Ecto.NoResultsError` if the Invite does not exist.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> get_invite!(123, %User{id: 123, role: :admin})
 | 
			
		||||
      %Invite{}
 | 
			
		||||
 | 
			
		||||
      iex> get_invite!(456, %User{id: 123, role: :admin})
 | 
			
		||||
      ** (Ecto.NoResultsError)
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec get_invite!(Invite.id(), User.t()) :: Invite.t()
 | 
			
		||||
  def get_invite!(id, %User{role: :admin}) do
 | 
			
		||||
    Repo.get!(Invite, id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns a valid invite or nil based on the attempted token
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> get_invite_by_token("valid_token")
 | 
			
		||||
      %Invite{}
 | 
			
		||||
 | 
			
		||||
      iex> get_invite_by_token("invalid_token")
 | 
			
		||||
      nil
 | 
			
		||||
  """
 | 
			
		||||
  @spec get_invite_by_token(token :: String.t() | nil) :: Invite.t() | nil
 | 
			
		||||
  def get_invite_by_token(nil), do: nil
 | 
			
		||||
  def get_invite_by_token(""), do: nil
 | 
			
		||||
 | 
			
		||||
  def get_invite_by_token(token) do
 | 
			
		||||
    Repo.one(
 | 
			
		||||
      from(i in Invite,
 | 
			
		||||
        where: i.token == ^token and i.disabled_at |> is_nil()
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Uses invite by decrementing uses_left, or marks invite invalid if it's been
 | 
			
		||||
  completely used.
 | 
			
		||||
  """
 | 
			
		||||
  @spec use_invite!(Invite.t()) :: Invite.t()
 | 
			
		||||
  def use_invite!(%Invite{uses_left: nil} = invite), do: invite
 | 
			
		||||
 | 
			
		||||
  def use_invite!(%Invite{uses_left: uses_left} = invite) do
 | 
			
		||||
    new_uses_left = uses_left - 1
 | 
			
		||||
 | 
			
		||||
    attrs =
 | 
			
		||||
      if new_uses_left <= 0 do
 | 
			
		||||
        now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
 | 
			
		||||
        %{"uses_left" => 0, "disabled_at" => now}
 | 
			
		||||
      else
 | 
			
		||||
        %{"uses_left" => new_uses_left}
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    invite |> Invite.update_changeset(attrs) |> Repo.update!()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Creates a invite.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> create_invite(%User{id: 123, role: :admin}, %{field: value})
 | 
			
		||||
      {:ok, %Invite{}}
 | 
			
		||||
 | 
			
		||||
      iex> create_invite(%User{id: 123, role: :admin}, %{field: bad_value})
 | 
			
		||||
      {:error, %Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec create_invite(User.t(), attrs :: map()) ::
 | 
			
		||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.new_invite())}
 | 
			
		||||
  def create_invite(%User{id: user_id, role: :admin}, attrs) do
 | 
			
		||||
    token =
 | 
			
		||||
      :crypto.strong_rand_bytes(@invite_token_length)
 | 
			
		||||
      |> Base.url_encode64()
 | 
			
		||||
      |> binary_part(0, @invite_token_length)
 | 
			
		||||
 | 
			
		||||
    attrs = attrs |> Map.merge(%{"user_id" => user_id, "token" => token})
 | 
			
		||||
 | 
			
		||||
    %Invite{} |> Invite.create_changeset(attrs) |> Repo.insert()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Updates a invite.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> update_invite(invite, %{field: new_value}, %User{id: 123, role: :admin})
 | 
			
		||||
      {:ok, %Invite{}}
 | 
			
		||||
 | 
			
		||||
      iex> update_invite(invite, %{field: bad_value}, %User{id: 123, role: :admin})
 | 
			
		||||
      {:error, %Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec update_invite(Invite.t(), attrs :: map(), User.t()) ::
 | 
			
		||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.t())}
 | 
			
		||||
  def update_invite(invite, attrs, %User{role: :admin}),
 | 
			
		||||
    do: invite |> Invite.update_changeset(attrs) |> Repo.update()
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes a invite.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
			
		||||
      {:ok, %Invite{}}
 | 
			
		||||
 | 
			
		||||
      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
			
		||||
      {:error, %Changeset{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec delete_invite(Invite.t(), User.t()) ::
 | 
			
		||||
          {:ok, Invite.t()} | {:error, Changeset.t(Invite.t())}
 | 
			
		||||
  def delete_invite(invite, %User{role: :admin}), do: invite |> Repo.delete()
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Deletes a invite.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> delete_invite(invite, %User{id: 123, role: :admin})
 | 
			
		||||
      %Invite{}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec delete_invite!(Invite.t(), User.t()) :: Invite.t()
 | 
			
		||||
  def delete_invite!(invite, %User{role: :admin}), do: invite |> Repo.delete!()
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Returns an `%Changeset{}` for tracking invite changes.
 | 
			
		||||
 | 
			
		||||
  ## Examples
 | 
			
		||||
 | 
			
		||||
      iex> change_invite(invite)
 | 
			
		||||
      %Changeset{data: %Invite{}}
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  @spec change_invite(Invite.t() | Invite.new_invite()) ::
 | 
			
		||||
          Changeset.t(Invite.t() | Invite.new_invite())
 | 
			
		||||
  @spec change_invite(Invite.t() | Invite.new_invite(), attrs :: map()) ::
 | 
			
		||||
          Changeset.t(Invite.t() | Invite.new_invite())
 | 
			
		||||
  def change_invite(invite, attrs \\ %{}), do: invite |> Invite.update_changeset(attrs)
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										57
									
								
								lib/lokal/invites/invite.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								lib/lokal/invites/invite.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
defmodule Lokal.Invites.Invite do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  An invite, created by an admin to allow someone to join their instance. An
 | 
			
		||||
  invite can be enabled or disabled, and can have an optional number of uses if
 | 
			
		||||
  `:uses_left` is defined.
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Ecto.Schema
 | 
			
		||||
  import Ecto.Changeset
 | 
			
		||||
  alias Ecto.{Changeset, UUID}
 | 
			
		||||
  alias Lokal.{Accounts.User, Invites.Invite}
 | 
			
		||||
 | 
			
		||||
  @primary_key {:id, :binary_id, autogenerate: true}
 | 
			
		||||
  @foreign_key_type :binary_id
 | 
			
		||||
  schema "invites" do
 | 
			
		||||
    field :name, :string
 | 
			
		||||
    field :token, :string
 | 
			
		||||
    field :uses_left, :integer, default: nil
 | 
			
		||||
    field :disabled_at, :naive_datetime
 | 
			
		||||
 | 
			
		||||
    belongs_to :user, User
 | 
			
		||||
 | 
			
		||||
    timestamps()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @type t :: %Invite{
 | 
			
		||||
          id: id(),
 | 
			
		||||
          name: String.t(),
 | 
			
		||||
          token: String.t(),
 | 
			
		||||
          uses_left: integer() | nil,
 | 
			
		||||
          disabled_at: NaiveDateTime.t(),
 | 
			
		||||
          user: User.t(),
 | 
			
		||||
          user_id: User.id(),
 | 
			
		||||
          inserted_at: NaiveDateTime.t(),
 | 
			
		||||
          updated_at: NaiveDateTime.t()
 | 
			
		||||
        }
 | 
			
		||||
  @type new_invite :: %Invite{}
 | 
			
		||||
  @type id :: UUID.t()
 | 
			
		||||
 | 
			
		||||
  @doc false
 | 
			
		||||
  @spec create_changeset(new_invite(), attrs :: map()) :: Changeset.t(new_invite())
 | 
			
		||||
  def create_changeset(invite, attrs) do
 | 
			
		||||
    invite
 | 
			
		||||
    |> cast(attrs, [:name, :token, :uses_left, :disabled_at, :user_id])
 | 
			
		||||
    |> validate_required([:name, :token, :user_id])
 | 
			
		||||
    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc false
 | 
			
		||||
  @spec update_changeset(t() | new_invite(), attrs :: map()) :: Changeset.t(t() | new_invite())
 | 
			
		||||
  def update_changeset(invite, attrs) do
 | 
			
		||||
    invite
 | 
			
		||||
    |> cast(attrs, [:name, :uses_left, :disabled_at])
 | 
			
		||||
    |> validate_required([:name, :token, :user_id])
 | 
			
		||||
    |> validate_number(:uses_left, greater_than_or_equal_to: 0)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										52
									
								
								lib/lokal_web/components/invite_card.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/lokal_web/components/invite_card.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
defmodule LokalWeb.Components.InviteCard do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display card for an invite
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use LokalWeb, :component
 | 
			
		||||
  alias LokalWeb.Endpoint
 | 
			
		||||
 | 
			
		||||
  def invite_card(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
 | 
			
		||||
      border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
      transition-all duration-300 ease-in-out"
 | 
			
		||||
    >
 | 
			
		||||
      <h1 class="title text-xl">
 | 
			
		||||
        <%= @invite.name %>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <%= if @invite.disabled_at |> is_nil() do %>
 | 
			
		||||
        <h2 class="title text-md">
 | 
			
		||||
          <%= gettext("Uses Left:") %>
 | 
			
		||||
          <%= @invite.uses_left || "Unlimited" %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% else %>
 | 
			
		||||
        <h2 class="title text-md">
 | 
			
		||||
          <%= gettext("Invite Disabled") %>
 | 
			
		||||
        </h2>
 | 
			
		||||
      <% end %>
 | 
			
		||||
 | 
			
		||||
      <div class="flex flex-row flex-wrap justify-center items-center">
 | 
			
		||||
        <code
 | 
			
		||||
          id={"code-#{@invite.id}"}
 | 
			
		||||
          class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
 | 
			
		||||
        >
 | 
			
		||||
          <%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %>
 | 
			
		||||
        </code>
 | 
			
		||||
 | 
			
		||||
        <%= if @code_actions do %>
 | 
			
		||||
          <%= render_slot(@code_actions) %>
 | 
			
		||||
        <% end %>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <%= if @inner_block do %>
 | 
			
		||||
        <div class="flex space-x-4 justify-center items-center">
 | 
			
		||||
          <%= render_slot(@inner_block) %>
 | 
			
		||||
        </div>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										37
									
								
								lib/lokal_web/components/user_card.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								lib/lokal_web/components/user_card.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
defmodule LokalWeb.Components.UserCard do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Display card for a user
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use LokalWeb, :component
 | 
			
		||||
 | 
			
		||||
  def user_card(assigns) do
 | 
			
		||||
    ~H"""
 | 
			
		||||
    <div
 | 
			
		||||
      id={"user-#{@user.id}"}
 | 
			
		||||
      class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
 | 
			
		||||
          border border-gray-400 rounded-lg shadow-lg hover:shadow-md
 | 
			
		||||
          transition-all duration-300 ease-in-out"
 | 
			
		||||
    >
 | 
			
		||||
      <h1 class="px-4 py-2 rounded-lg title text-xl break-all">
 | 
			
		||||
        <%= @user.email %>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <h3 class="px-4 py-2 rounded-lg title text-lg">
 | 
			
		||||
        <%= if @user.confirmed_at |> is_nil() do %>
 | 
			
		||||
          Email unconfirmed
 | 
			
		||||
        <% else %>
 | 
			
		||||
          <p>User was confirmed at</p>
 | 
			
		||||
          <%= @user.confirmed_at |> display_datetime() %>
 | 
			
		||||
        <% end %>
 | 
			
		||||
      </h3>
 | 
			
		||||
 | 
			
		||||
      <%= if @inner_block do %>
 | 
			
		||||
        <div class="px-4 py-2 flex space-x-4 justify-center items-center">
 | 
			
		||||
          <%= render_slot(@inner_block) %>
 | 
			
		||||
        </div>
 | 
			
		||||
      <% end %>
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Reference in New Issue
	
	Block a user