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