add invites
This commit is contained in:
		| @@ -4,8 +4,9 @@ defmodule Lokal.Accounts do | |||||||
|   """ |   """ | ||||||
|  |  | ||||||
|   import Ecto.Query, warn: false |   import Ecto.Query, warn: false | ||||||
|   alias Lokal.Repo |   alias Lokal.{Mailer, Repo} | ||||||
|   alias Lokal.Accounts.{User, UserNotifier, UserToken} |   alias Lokal.Accounts.{User, UserToken} | ||||||
|  |   alias Ecto.{Changeset, Multi} | ||||||
|  |  | ||||||
|   ## Database getters |   ## Database getters | ||||||
|  |  | ||||||
| @@ -21,9 +22,8 @@ defmodule Lokal.Accounts do | |||||||
|       nil |       nil | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   def get_user_by_email(email) when is_binary(email) do |   @spec get_user_by_email(String.t()) :: User.t() | nil | ||||||
|     Repo.get_by(User, email: email) |   def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email) | ||||||
|   end |  | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Gets a user by email and password. |   Gets a user by email and password. | ||||||
| @@ -37,6 +37,8 @@ defmodule Lokal.Accounts do | |||||||
|       nil |       nil | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|  |   @spec get_user_by_email_and_password(String.t(), String.t()) :: | ||||||
|  |           User.t() | nil | ||||||
|   def get_user_by_email_and_password(email, password) |   def get_user_by_email_and_password(email, password) | ||||||
|       when is_binary(email) and is_binary(password) do |       when is_binary(email) and is_binary(password) do | ||||||
|     user = Repo.get_by(User, email: email) |     user = Repo.get_by(User, email: email) | ||||||
| @@ -57,8 +59,38 @@ defmodule Lokal.Accounts do | |||||||
|       ** (Ecto.NoResultsError) |       ** (Ecto.NoResultsError) | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|  |   @spec get_user!(User.t()) :: User.t() | ||||||
|   def get_user!(id), do: Repo.get!(User, id) |   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 |   ## User registration | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
| @@ -70,42 +102,61 @@ defmodule Lokal.Accounts do | |||||||
|       {:ok, %User{}} |       {:ok, %User{}} | ||||||
|  |  | ||||||
|       iex> register_user(%{field: bad_value}) |       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 |   def register_user(attrs) do | ||||||
|     %User{} |     # if no registered users, make first user an admin | ||||||
|     |> User.registration_changeset(attrs) |     role = | ||||||
|     |> Repo.insert() |       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 |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns an `%Ecto.Changeset{}` for tracking user changes. |   Returns an `%Changeset{}` for tracking user changes. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> change_user_registration(user) |       iex> change_user_registration(user) | ||||||
|       %Ecto.Changeset{data: %User{}} |       %Changeset{data: %User{}} | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   def change_user_registration(%User{} = user, attrs \\ %{}) do |   @spec change_user_registration(User.t() | User.new_user()) :: | ||||||
|     User.registration_changeset(user, attrs, hash_password: false) |           Changeset.t(User.t() | User.new_user()) | ||||||
|   end |   @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 |   ## Settings | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns an `%Ecto.Changeset{}` for changing the user email. |   Returns an `%Changeset{}` for changing the user email. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> change_user_email(user) |       iex> change_user_email(user) | ||||||
|       %Ecto.Changeset{data: %User{}} |       %Changeset{data: %User{}} | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   def change_user_email(user, attrs \\ %{}) do |   @spec change_user_email(User.t(), map()) :: Changeset.t(User.t()) | ||||||
|     User.email_changeset(user, attrs) |   def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs) | ||||||
|   end |  | ||||||
|  |   @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 """ |   @doc """ | ||||||
|   Emulates that the email will change without actually changing |   Emulates that the email will change without actually changing | ||||||
| @@ -117,14 +168,16 @@ defmodule Lokal.Accounts do | |||||||
|       {:ok, %User{}} |       {:ok, %User{}} | ||||||
|  |  | ||||||
|       iex> apply_user_email(user, "invalid password", %{email: ...}) |       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 |   def apply_user_email(user, password, attrs) do | ||||||
|     user |     user | ||||||
|     |> User.email_changeset(attrs) |     |> User.email_changeset(attrs) | ||||||
|     |> User.validate_current_password(password) |     |> User.validate_current_password(password) | ||||||
|     |> Ecto.Changeset.apply_action(:update) |     |> Changeset.apply_action(:update) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
| @@ -133,6 +186,7 @@ defmodule Lokal.Accounts do | |||||||
|   If the token matches, the user email is updated and the token is deleted. |   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. |   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 |   def update_user_email(user, token) do | ||||||
|     context = "change:#{user.email}" |     context = "change:#{user.email}" | ||||||
|  |  | ||||||
| @@ -145,12 +199,13 @@ defmodule Lokal.Accounts do | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   @spec user_email_multi(User.t(), String.t(), String.t()) :: Multi.t() | ||||||
|   defp user_email_multi(user, email, context) do |   defp user_email_multi(user, email, context) do | ||||||
|     changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset() |     changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset() | ||||||
|  |  | ||||||
|     Ecto.Multi.new() |     Multi.new() | ||||||
|     |> Ecto.Multi.update(:user, changeset) |     |> Multi.update(:user, changeset) | ||||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context])) |     |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context])) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
| @@ -162,26 +217,26 @@ defmodule Lokal.Accounts do | |||||||
|       {:ok, %{to: ..., body: ...}} |       {: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 |       when is_function(update_email_url_fun, 1) do | ||||||
|     {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") |     {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") | ||||||
|  |  | ||||||
|     Repo.insert!(user_token) |     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 |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Returns an `%Ecto.Changeset{}` for changing the user password. |   Returns an `%Changeset{}` for changing the user password. | ||||||
|  |  | ||||||
|   ## Examples |   ## Examples | ||||||
|  |  | ||||||
|       iex> change_user_password(user) |       iex> change_user_password(user) | ||||||
|       %Ecto.Changeset{data: %User{}} |       %Changeset{data: %User{}} | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|   def change_user_password(user, attrs \\ %{}) do |   @spec change_user_password(User.t(), map()) :: Changeset.t(User.t()) | ||||||
|     User.password_changeset(user, attrs, hash_password: false) |   def change_user_password(user, attrs \\ %{}), | ||||||
|   end |     do: User.password_changeset(user, attrs, hash_password: false) | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Updates the user password. |   Updates the user password. | ||||||
| @@ -192,18 +247,20 @@ defmodule Lokal.Accounts do | |||||||
|       {:ok, %User{}} |       {:ok, %User{}} | ||||||
|  |  | ||||||
|       iex> update_user_password(user, "invalid password", %{password: ...}) |       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 |   def update_user_password(user, password, attrs) do | ||||||
|     changeset = |     changeset = | ||||||
|       user |       user | ||||||
|       |> User.password_changeset(attrs) |       |> User.password_changeset(attrs) | ||||||
|       |> User.validate_current_password(password) |       |> User.validate_current_password(password) | ||||||
|  |  | ||||||
|     Ecto.Multi.new() |     Multi.new() | ||||||
|     |> Ecto.Multi.update(:user, changeset) |     |> Multi.update(:user, changeset) | ||||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) |     |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) | ||||||
|     |> Repo.transaction() |     |> Repo.transaction() | ||||||
|     |> case do |     |> case do | ||||||
|       {:ok, %{user: user}} -> {:ok, user} |       {:ok, %{user: user}} -> {:ok, user} | ||||||
| @@ -211,11 +268,28 @@ defmodule Lokal.Accounts do | |||||||
|     end |     end | ||||||
|   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 |   ## Session | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
|   Generates a session token. |   Generates a session token. | ||||||
|   """ |   """ | ||||||
|  |   @spec generate_user_session_token(User.t()) :: String.t() | ||||||
|   def generate_user_session_token(user) do |   def generate_user_session_token(user) do | ||||||
|     {token, user_token} = UserToken.build_session_token(user) |     {token, user_token} = UserToken.build_session_token(user) | ||||||
|     Repo.insert!(user_token) |     Repo.insert!(user_token) | ||||||
| @@ -225,6 +299,7 @@ defmodule Lokal.Accounts do | |||||||
|   @doc """ |   @doc """ | ||||||
|   Gets the user with the given signed token. |   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 |   def get_user_by_session_token(token) do | ||||||
|     {:ok, query} = UserToken.verify_session_token_query(token) |     {:ok, query} = UserToken.verify_session_token_query(token) | ||||||
|     Repo.one(query) |     Repo.one(query) | ||||||
| @@ -233,11 +308,30 @@ defmodule Lokal.Accounts do | |||||||
|   @doc """ |   @doc """ | ||||||
|   Deletes the signed token with the given context. |   Deletes the signed token with the given context. | ||||||
|   """ |   """ | ||||||
|  |   @spec delete_session_token(String.t()) :: :ok | ||||||
|   def delete_session_token(token) do |   def delete_session_token(token) do | ||||||
|     Repo.delete_all(UserToken.token_and_context_query(token, "session")) |     Repo.delete_all(UserToken.token_and_context_query(token, "session")) | ||||||
|     :ok |     :ok | ||||||
|   end |   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 |   ## Confirmation | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
| @@ -252,14 +346,15 @@ defmodule Lokal.Accounts do | |||||||
|       {:error, :already_confirmed} |       {: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 |       when is_function(confirmation_url_fun, 1) do | ||||||
|     if user.confirmed_at do |     if user.confirmed_at do | ||||||
|       {:error, :already_confirmed} |       {:error, :already_confirmed} | ||||||
|     else |     else | ||||||
|       {encoded_token, user_token} = UserToken.build_email_token(user, "confirm") |       {encoded_token, user_token} = UserToken.build_email_token(user, "confirm") | ||||||
|       Repo.insert!(user_token) |       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 | ||||||
|   end |   end | ||||||
|  |  | ||||||
| @@ -269,6 +364,7 @@ defmodule Lokal.Accounts do | |||||||
|   If the token matches, the user account is marked as confirmed |   If the token matches, the user account is marked as confirmed | ||||||
|   and the token is deleted. |   and the token is deleted. | ||||||
|   """ |   """ | ||||||
|  |   @spec confirm_user(String.t()) :: {:ok, User.t()} | atom() | ||||||
|   def confirm_user(token) do |   def confirm_user(token) do | ||||||
|     with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"), |     with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"), | ||||||
|          %User{} = user <- Repo.one(query), |          %User{} = user <- Repo.one(query), | ||||||
| @@ -279,10 +375,11 @@ defmodule Lokal.Accounts do | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   @spec confirm_user_multi(User.t()) :: Multi.t() | ||||||
|   def confirm_user_multi(user) do |   def confirm_user_multi(user) do | ||||||
|     Ecto.Multi.new() |     Multi.new() | ||||||
|     |> Ecto.Multi.update(:user, User.confirm_changeset(user)) |     |> Multi.update(:user, User.confirm_changeset(user)) | ||||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"])) |     |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"])) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   ## Reset password |   ## Reset password | ||||||
| @@ -296,11 +393,12 @@ defmodule Lokal.Accounts do | |||||||
|       {:ok, %{to: ..., body: ...}} |       {: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 |       when is_function(reset_password_url_fun, 1) do | ||||||
|     {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password") |     {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password") | ||||||
|     Repo.insert!(user_token) |     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 |   end | ||||||
|  |  | ||||||
|   @doc """ |   @doc """ | ||||||
| @@ -315,6 +413,7 @@ defmodule Lokal.Accounts do | |||||||
|       nil |       nil | ||||||
|  |  | ||||||
|   """ |   """ | ||||||
|  |   @spec get_user_by_reset_password_token(String.t()) :: User.t() | nil | ||||||
|   def get_user_by_reset_password_token(token) do |   def get_user_by_reset_password_token(token) do | ||||||
|     with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"), |     with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"), | ||||||
|          %User{} = user <- Repo.one(query) do |          %User{} = user <- Repo.one(query) do | ||||||
| @@ -333,13 +432,14 @@ defmodule Lokal.Accounts do | |||||||
|       {:ok, %User{}} |       {:ok, %User{}} | ||||||
|  |  | ||||||
|       iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"}) |       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 |   def reset_user_password(user, attrs) do | ||||||
|     Ecto.Multi.new() |     Multi.new() | ||||||
|     |> Ecto.Multi.update(:user, User.password_changeset(user, attrs)) |     |> Multi.update(:user, User.password_changeset(user, attrs)) | ||||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) |     |> Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) | ||||||
|     |> Repo.transaction() |     |> Repo.transaction() | ||||||
|     |> case do |     |> case do | ||||||
|       {:ok, %{user: user}} -> {:ok, user} |       {: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 | ||||||
| @@ -9,6 +9,7 @@ defmodule Lokal.Repo.Migrations.CreateUsersAuthTables do | |||||||
|       add :email, :citext, null: false |       add :email, :citext, null: false | ||||||
|       add :hashed_password, :string, null: false |       add :hashed_password, :string, null: false | ||||||
|       add :confirmed_at, :naive_datetime |       add :confirmed_at, :naive_datetime | ||||||
|  |       add :role, :string | ||||||
|       timestamps() |       timestamps() | ||||||
|     end |     end | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								priv/repo/migrations/20210904211727_create_invites.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								priv/repo/migrations/20210904211727_create_invites.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | defmodule Lokal.Repo.Migrations.CreateInvites do | ||||||
|  |   use Ecto.Migration | ||||||
|  |  | ||||||
|  |   def change do | ||||||
|  |     create table(:invites, primary_key: false) do | ||||||
|  |       add :id, :binary_id, primary_key: true | ||||||
|  |       add :name, :string | ||||||
|  |       add :token, :string | ||||||
|  |       add :uses_left, :integer, default: nil | ||||||
|  |       add :disabled_at, :naive_datetime, default: nil | ||||||
|  |  | ||||||
|  |       add :user_id, references(:users, on_delete: :delete_all, type: :binary_id) | ||||||
|  |  | ||||||
|  |       timestamps() | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     create index(:invites, [:user_id]) | ||||||
|  |   end | ||||||
|  | end | ||||||
		Reference in New Issue
	
	Block a user