diff --git a/lib/lokal/accounts.ex b/lib/lokal/accounts.ex index 1f2eaba..e392613 100644 --- a/lib/lokal/accounts.ex +++ b/lib/lokal/accounts.ex @@ -5,7 +5,7 @@ defmodule Lokal.Accounts do import Ecto.Query, warn: false alias Lokal.{Mailer, Repo} - alias Lokal.Accounts.{User, UserToken} + alias Lokal.Accounts.{Invite, Invites, User, UserToken} alias Ecto.{Changeset, Multi} alias Oban.Job @@ -25,7 +25,9 @@ defmodule Lokal.Accounts do """ @spec get_user_by_email(email :: String.t()) :: User.t() | nil - def get_user_by_email(email) when is_binary(email), do: 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 """ Gets a user by email and password. @@ -64,7 +66,9 @@ defmodule Lokal.Accounts do """ @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) + end @doc """ Returns all users grouped by role. @@ -113,10 +117,18 @@ defmodule Lokal.Accounts do :passed """ - @spec register_user(attrs :: map()) :: {:ok, User.t()} | {:error, User.changeset()} - def register_user(attrs) do + @spec register_user(attrs :: map(), Invite.token() | nil) :: + {:ok, User.t()} | {:error, :invalid_token | User.changeset()} + def register_user(attrs, invite_token \\ nil) do Multi.new() |> Multi.one(:users_count, from(u in User, select: count(u.id), distinct: true)) + |> Multi.run(:use_invite, fn _changes_so_far, _repo -> + if allow_registration?() and invite_token |> is_nil() do + {:ok, :invite_not_required} + else + Invites.use_invite(invite_token) + end + end) |> Multi.insert(:add_user, fn %{users_count: count} -> # if no registered users, make first user an admin role = if count == 0, do: :admin, else: :user @@ -126,6 +138,7 @@ defmodule Lokal.Accounts do |> Repo.transaction() |> case do {:ok, %{add_user: user}} -> {:ok, user} + {:error, :use_invite, :invalid_token, _changes_so_far} -> {:error, :invalid_token} {:error, :add_user, changeset, _changes_so_far} -> {:error, changeset} end end @@ -144,8 +157,9 @@ defmodule Lokal.Accounts do """ @spec change_user_registration() :: User.changeset() @spec change_user_registration(attrs :: map()) :: User.changeset() - def change_user_registration(attrs \\ %{}), - do: User.registration_changeset(attrs, hash_password: false) + def change_user_registration(attrs \\ %{}) do + User.registration_changeset(attrs, hash_password: false) + end ## Settings @@ -160,7 +174,9 @@ defmodule Lokal.Accounts do """ @spec change_user_email(User.t()) :: User.changeset() @spec change_user_email(User.t(), attrs :: map()) :: User.changeset() - def change_user_email(user, attrs \\ %{}), do: 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. @@ -172,7 +188,9 @@ defmodule Lokal.Accounts do """ @spec change_user_role(User.t(), User.role()) :: User.changeset() - def change_user_role(user, role), do: User.role_changeset(user, role) + def change_user_role(user, role) do + User.role_changeset(user, role) + end @doc """ Emulates that the email will change without actually changing @@ -262,8 +280,9 @@ defmodule Lokal.Accounts do """ @spec change_user_password(User.t(), attrs :: map()) :: User.changeset() - def change_user_password(user, attrs \\ %{}), - do: User.password_changeset(user, attrs, hash_password: false) + def change_user_password(user, attrs \\ %{}) do + User.password_changeset(user, attrs, hash_password: false) + end @doc """ Updates the user password. @@ -314,7 +333,9 @@ defmodule Lokal.Accounts do """ @spec change_user_locale(User.t()) :: User.changeset() - def change_user_locale(%{locale: locale} = user), do: User.locale_changeset(user, locale) + def change_user_locale(%{locale: locale} = user) do + User.locale_changeset(user, locale) + end @doc """ Updates the user locale. @@ -328,8 +349,9 @@ defmodule Lokal.Accounts do """ @spec update_user_locale(User.t(), locale :: String.t()) :: {:ok, User.t()} | {:error, User.changeset()} - def update_user_locale(user, locale), - do: user |> User.locale_changeset(locale) |> Repo.update() + def update_user_locale(user, locale) do + user |> User.locale_changeset(locale) |> Repo.update() + end @doc """ Deletes a user. must be performed by an admin or the same user! @@ -346,8 +368,13 @@ defmodule Lokal.Accounts do """ @spec delete_user!(user_to_delete :: 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!() + def delete_user!(user, %User{role: :admin}) do + user |> Repo.delete!() + end + + def delete_user!(%User{id: user_id} = user, %User{id: user_id}) do + user |> Repo.delete!() + end ## Session @@ -375,7 +402,7 @@ defmodule Lokal.Accounts do """ @spec delete_session_token(token :: String.t()) :: :ok def delete_session_token(token) do - Repo.delete_all(UserToken.token_and_context_query(token, "session")) + UserToken.token_and_context_query(token, "session") |> Repo.delete_all() :ok end diff --git a/lib/lokal/invites/invite.ex b/lib/lokal/accounts/invite.ex similarity index 70% rename from lib/lokal/invites/invite.ex rename to lib/lokal/accounts/invite.ex index f130158..00c15ae 100644 --- a/lib/lokal/invites/invite.ex +++ b/lib/lokal/accounts/invite.ex @@ -1,4 +1,4 @@ -defmodule Lokal.Invites.Invite do +defmodule Lokal.Accounts.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 @@ -8,7 +8,7 @@ defmodule Lokal.Invites.Invite do use Ecto.Schema import Ecto.Changeset alias Ecto.{Changeset, UUID} - alias Lokal.{Accounts.User, Invites.Invite} + alias Lokal.Accounts.User @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @@ -23,10 +23,10 @@ defmodule Lokal.Invites.Invite do timestamps() end - @type t :: %Invite{ + @type t :: %__MODULE__{ id: id(), name: String.t(), - token: String.t(), + token: token(), uses_left: integer() | nil, disabled_at: NaiveDateTime.t(), user: User.t(), @@ -34,24 +34,27 @@ defmodule Lokal.Invites.Invite do inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } - @type new_invite :: %Invite{} + @type new_invite :: %__MODULE__{} @type id :: UUID.t() + @type changeset :: Changeset.t(t() | new_invite()) + @type token :: String.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 + @spec create_changeset(User.t(), token(), attrs :: map()) :: changeset() + def create_changeset(%User{id: user_id}, token, attrs) do + %__MODULE__{} + |> change(token: token, user_id: user_id) |> cast(attrs, [:name, :uses_left, :disabled_at]) |> 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() + def update_changeset(invite, attrs) do + invite + |> cast(attrs, [:name, :uses_left, :disabled_at]) + |> validate_required([:name]) + |> validate_number(:uses_left, greater_than_or_equal_to: 0) + end end diff --git a/lib/lokal/accounts/invites.ex b/lib/lokal/accounts/invites.ex new file mode 100644 index 0000000..71e953f --- /dev/null +++ b/lib/lokal/accounts/invites.ex @@ -0,0 +1,187 @@ +defmodule Lokal.Accounts.Invites do + @moduledoc """ + The Invites context. + """ + + import Ecto.Query, warn: false + alias Ecto.Multi + alias Lokal.Accounts.{Invite, User} + alias Lokal.Repo + + @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 for a user + + Raises `Ecto.NoResultsError` if the Invite does not exist. + + ## Examples + + iex> get_invite!(123, %User{id: 123, role: :admin}) + %Invite{} + + > 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 if an invite token is still valid + + ## Examples + + iex> valid_invite_token?("valid_token") + %Invite{} + + iex> valid_invite_token?("invalid_token") + nil + """ + @spec valid_invite_token?(Invite.token() | nil) :: boolean() + def valid_invite_token?(token) when token in [nil, ""], do: false + + def valid_invite_token?(token) do + Repo.exists?( + from i in Invite, + where: i.token == ^token, + where: 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.token()) :: {:ok, Invite.t()} | {:error, :invalid_token} + def use_invite(invite_token) do + Multi.new() + |> Multi.run(:invite, fn _changes_so_far, _repo -> + invite_token |> get_invite_by_token() + end) + |> Multi.update(:decrement_invite, fn %{invite: invite} -> + decrement_invite_changeset(invite) + end) + |> Repo.transaction() + |> case do + {:ok, %{decrement_invite: invite}} -> {:ok, invite} + {:error, :invite, :invalid_token, _changes_so_far} -> {:error, :invalid_token} + end + end + + @spec get_invite_by_token(Invite.token()) :: {:ok, Invite.t()} | {:error, :invalid_token} + defp get_invite_by_token(token) do + Repo.one( + from i in Invite, + where: i.token == ^token, + where: i.disabled_at |> is_nil() + ) + |> case do + nil -> {:error, :invalid_token} + invite -> {:ok, invite} + end + end + + @spec decrement_invite_changeset(Invite.t()) :: Invite.changeset() + defp decrement_invite_changeset(%Invite{uses_left: nil} = invite) do + invite |> Invite.update_changeset(%{}) + end + + defp decrement_invite_changeset(%Invite{uses_left: 1} = invite) do + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + invite |> Invite.update_changeset(%{uses_left: 0, disabled_at: now}) + end + + defp decrement_invite_changeset(%Invite{uses_left: uses_left} = invite) do + invite |> Invite.update_changeset(%{uses_left: uses_left - 1}) + 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, Invite.changeset()} + def create_invite(%User{role: :admin} = user, attrs) do + token = + :crypto.strong_rand_bytes(@invite_token_length) + |> Base.url_encode64() + |> binary_part(0, @invite_token_length) + + Invite.create_changeset(user, token, 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, Invite.changeset()} + def update_invite(invite, attrs, %User{role: :admin}) do + invite |> Invite.update_changeset(attrs) |> Repo.update() + end + + @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, Invite.changeset()} + def delete_invite(invite, %User{role: :admin}) do + invite |> Repo.delete() + end + + @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!() + end +end diff --git a/lib/lokal/accounts/user.ex b/lib/lokal/accounts/user.ex index 1d5d014..e05a090 100644 --- a/lib/lokal/accounts/user.ex +++ b/lib/lokal/accounts/user.ex @@ -7,7 +7,7 @@ defmodule Lokal.Accounts.User do import Ecto.Changeset import LokalWeb.Gettext alias Ecto.{Changeset, UUID} - alias Lokal.{Accounts.User, Invites.Invite} + alias Lokal.Accounts.{Invite, User} @derive {Jason.Encoder, only: [ diff --git a/lib/lokal/accounts/user_token.ex b/lib/lokal/accounts/user_token.ex index e37c77f..5cc012d 100644 --- a/lib/lokal/accounts/user_token.ex +++ b/lib/lokal/accounts/user_token.ex @@ -5,6 +5,8 @@ defmodule Lokal.Accounts.UserToken do use Ecto.Schema import Ecto.Query + alias Ecto.{Association, UUID} + alias Lokal.Accounts.User @hash_algorithm :sha256 @rand_size 32 @@ -22,11 +24,25 @@ defmodule Lokal.Accounts.UserToken do field :token, :binary field :context, :string field :sent_to, :string - belongs_to :user, Lokal.Accounts.User + + belongs_to :user, User timestamps(updated_at: false) end + @type t :: %__MODULE__{ + id: id(), + token: token(), + context: String.t(), + sent_to: String.t(), + user: User.t() | Association.NotLoaded.t(), + user_id: User.id() | nil, + inserted_at: NaiveDateTime.t() + } + @type new_user_token :: %__MODULE__{} + @type id :: UUID.t() + @type token :: binary() + @doc """ Generates a token that will be stored in a signed place, such as session or cookie. As they are signed, those @@ -34,7 +50,7 @@ defmodule Lokal.Accounts.UserToken do """ def build_session_token(user) do token = :crypto.strong_rand_bytes(@rand_size) - {token, %Lokal.Accounts.UserToken{token: token, context: "session", user_id: user.id}} + {token, %__MODULE__{token: token, context: "session", user_id: user.id}} end @doc """ @@ -69,7 +85,7 @@ defmodule Lokal.Accounts.UserToken do hashed_token = :crypto.hash(@hash_algorithm, token) {Base.url_encode64(token, padding: false), - %Lokal.Accounts.UserToken{ + %__MODULE__{ token: hashed_token, context: context, sent_to: sent_to, @@ -129,17 +145,17 @@ defmodule Lokal.Accounts.UserToken do Returns the given token with the given context. """ def token_and_context_query(token, context) do - from Lokal.Accounts.UserToken, where: [token: ^token, context: ^context] + from __MODULE__, where: [token: ^token, context: ^context] end @doc """ Gets all tokens for the given user for the given contexts. """ def user_and_contexts_query(user, :all) do - from t in Lokal.Accounts.UserToken, where: t.user_id == ^user.id + from t in __MODULE__, where: t.user_id == ^user.id end def user_and_contexts_query(user, [_ | _] = contexts) do - from t in Lokal.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts + from t in __MODULE__, where: t.user_id == ^user.id and t.context in ^contexts end end diff --git a/lib/lokal/invites.ex b/lib/lokal/invites.ex deleted file mode 100644 index 4e03b37..0000000 --- a/lib/lokal/invites.ex +++ /dev/null @@ -1,173 +0,0 @@ -defmodule Lokal.Invites do - @moduledoc """ - The Invites context. - """ - - import Ecto.Query, warn: false - alias Ecto.Changeset - alias Lokal.{Accounts.User, Invites.Invite, Repo} - - @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 diff --git a/lib/lokal_web/controllers/user_registration_controller.ex b/lib/lokal_web/controllers/user_registration_controller.ex index 231ee5f..11e5f5f 100644 --- a/lib/lokal_web/controllers/user_registration_controller.ex +++ b/lib/lokal_web/controllers/user_registration_controller.ex @@ -1,14 +1,12 @@ defmodule LokalWeb.UserRegistrationController do use LokalWeb, :controller import LokalWeb.Gettext - alias Lokal.{Accounts, Invites} + alias Lokal.{Accounts, Accounts.Invites} alias LokalWeb.{Endpoint, HomeLive} def new(conn, %{"invite" => invite_token}) do - invite = Invites.get_invite_by_token(invite_token) - - if invite do - conn |> render_new(invite) + if Invites.valid_invite_token?(invite_token) do + conn |> render_new(invite_token) else conn |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) @@ -27,19 +25,17 @@ defmodule LokalWeb.UserRegistrationController do end # renders new user registration page - defp render_new(conn, invite \\ nil) do + defp render_new(conn, invite_token \\ nil) do render(conn, "new.html", changeset: Accounts.change_user_registration(), - invite: invite, + invite_token: invite_token, page_title: gettext("Register") ) end def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do - invite = Invites.get_invite_by_token(invite_token) - - if invite do - conn |> create_user(attrs, invite) + if Invites.valid_invite_token?(invite_token) do + conn |> create_user(attrs, invite_token) else conn |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) @@ -57,13 +53,9 @@ defmodule LokalWeb.UserRegistrationController do end end - defp create_user(conn, %{"user" => user_params}, invite \\ nil) do - case Accounts.register_user(user_params) do + defp create_user(conn, %{"user" => user_params}, invite_token \\ nil) do + case Accounts.register_user(user_params, invite_token) do {:ok, user} -> - unless invite |> is_nil() do - invite |> Invites.use_invite!() - end - Accounts.deliver_user_confirmation_instructions( user, &Routes.user_confirmation_url(conn, :confirm, &1) @@ -73,8 +65,13 @@ defmodule LokalWeb.UserRegistrationController do |> put_flash(:info, dgettext("prompts", "Please check your email to verify your account")) |> redirect(to: Routes.user_session_path(Endpoint, :new)) + {:error, :invalid_token} -> + conn + |> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired")) + |> redirect(to: Routes.live_path(Endpoint, HomeLive)) + {:error, %Ecto.Changeset{} = changeset} -> - conn |> render("new.html", changeset: changeset, invite: invite) + conn |> render("new.html", changeset: changeset, invite_token: invite_token) end end end diff --git a/lib/lokal_web/live/invite_live/form_component.ex b/lib/lokal_web/live/invite_live/form_component.ex index c41c7e8..274a4fc 100644 --- a/lib/lokal_web/live/invite_live/form_component.ex +++ b/lib/lokal_web/live/invite_live/form_component.ex @@ -1,11 +1,11 @@ defmodule LokalWeb.InviteLive.FormComponent do @moduledoc """ - Livecomponent that can update or create an Lokal.Invites.Invite + Livecomponent that can update or create an Lokal.Accounts.Invite """ use LokalWeb, :live_component alias Ecto.Changeset - alias Lokal.{Accounts.User, Invites, Invites.Invite} + alias Lokal.Accounts.{Invite, Invites, User} alias Phoenix.LiveView.Socket @impl true @@ -13,23 +13,44 @@ defmodule LokalWeb.InviteLive.FormComponent do %{:invite => Invite.t(), :current_user => User.t(), optional(any) => any}, Socket.t() ) :: {:ok, Socket.t()} - def update(%{invite: invite} = assigns, socket) do - {:ok, socket |> assign(assigns) |> assign(:changeset, Invites.change_invite(invite))} + def update(%{invite: _invite} = assigns, socket) do + {:ok, socket |> assign(assigns) |> assign_changeset(%{})} end @impl true - def handle_event( - "validate", - %{"invite" => invite_params}, - %{assigns: %{invite: invite}} = socket - ) do - {:noreply, socket |> assign(:changeset, invite |> Invites.change_invite(invite_params))} + def handle_event("validate", %{"invite" => invite_params}, socket) do + {:noreply, socket |> assign_changeset(invite_params)} end def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do save_invite(socket, action, invite_params) end + defp assign_changeset( + %{assigns: %{action: action, current_user: user, invite: invite}} = socket, + invite_params + ) do + changeset_action = + case action do + :new -> :insert + :edit -> :update + end + + changeset = + case action do + :new -> Invite.create_changeset(user, "example_token", invite_params) + :edit -> invite |> Invite.update_changeset(invite_params) + end + + changeset = + case changeset |> Changeset.apply_action(changeset_action) do + {:ok, _data} -> changeset + {:error, changeset} -> changeset + end + + socket |> assign(:changeset, changeset) + end + defp save_invite( %{assigns: %{current_user: current_user, invite: invite, return_to: return_to}} = socket, :edit, @@ -38,10 +59,8 @@ defmodule LokalWeb.InviteLive.FormComponent do socket = case invite |> Invites.update_invite(invite_params, current_user) do {:ok, %{name: invite_name}} -> - prompt = - dgettext("prompts", "%{invite_name} updated successfully", invite_name: invite_name) - - socket |> put_flash(:info, prompt) |> push_redirect(to: return_to) + prompt = dgettext("prompts", "%{name} updated successfully", name: invite_name) + socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) {:error, %Changeset{} = changeset} -> socket |> assign(:changeset, changeset) @@ -58,10 +77,8 @@ defmodule LokalWeb.InviteLive.FormComponent do socket = case current_user |> Invites.create_invite(invite_params) do {:ok, %{name: invite_name}} -> - prompt = - dgettext("prompts", "%{invite_name} created successfully", invite_name: invite_name) - - socket |> put_flash(:info, prompt) |> push_redirect(to: return_to) + prompt = dgettext("prompts", "%{name} created successfully", name: invite_name) + socket |> put_flash(:info, prompt) |> push_navigate(to: return_to) {:error, %Changeset{} = changeset} -> socket |> assign(changeset: changeset) diff --git a/lib/lokal_web/live/invite_live/index.ex b/lib/lokal_web/live/invite_live/index.ex index 0e14e31..038bc47 100644 --- a/lib/lokal_web/live/invite_live/index.ex +++ b/lib/lokal_web/live/invite_live/index.ex @@ -1,11 +1,12 @@ defmodule LokalWeb.InviteLive.Index do @moduledoc """ - Liveview to show a Lokal.Invites.Invite index + Liveview to show a Lokal.Accounts.Invite index """ use LokalWeb, :live_view import LokalWeb.Components.{InviteCard, UserCard} - alias Lokal.{Accounts, Invites, Invites.Invite} + alias Lokal.Accounts + alias Lokal.Accounts.{Invite, Invites} alias LokalWeb.{Endpoint, HomeLive} alias Phoenix.LiveView.JS diff --git a/lib/lokal_web/templates/user_registration/new.html.heex b/lib/lokal_web/templates/user_registration/new.html.heex index 02ee97f..c1bd49e 100644 --- a/lib/lokal_web/templates/user_registration/new.html.heex +++ b/lib/lokal_web/templates/user_registration/new.html.heex @@ -13,8 +13,8 @@ <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>

- <%= if @invite do %> - <%= hidden_input(f, :invite_token, value: @invite.token) %> + <%= if @invite_token do %> + <%= hidden_input(f, :invite_token, value: @invite_token) %> <% end %> <%= label(f, :email, class: "title text-lg text-primary-600") %> diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 9400f55..1afbd99 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -37,7 +37,7 @@ msgid "Invite Disabled" msgstr "" #: lib/lokal_web/components/topbar.ex:61 -#: lib/lokal_web/live/invite_live/index.ex:41 +#: lib/lokal_web/live/invite_live/index.ex:42 #: lib/lokal_web/live/invite_live/index.html.heex:3 #, elixir-autogen, elixir-format msgid "Invites" @@ -63,7 +63,7 @@ msgstr "" msgid "Reconnecting..." msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:34 +#: lib/lokal_web/controllers/user_registration_controller.ex:32 #, elixir-autogen, elixir-format msgid "Register" msgstr "" @@ -94,7 +94,7 @@ msgstr "" msgid "Disable" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:33 +#: lib/lokal_web/live/invite_live/index.ex:34 #, elixir-autogen, elixir-format msgid "Edit Invite" msgstr "" @@ -109,7 +109,7 @@ msgstr "" msgid "Name" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:37 +#: lib/lokal_web/live/invite_live/index.ex:38 #, elixir-autogen, elixir-format msgid "New Invite" msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index df1429e..140a8d9 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -37,7 +37,7 @@ msgid "Invite Disabled" msgstr "" #: lib/lokal_web/components/topbar.ex:61 -#: lib/lokal_web/live/invite_live/index.ex:41 +#: lib/lokal_web/live/invite_live/index.ex:42 #: lib/lokal_web/live/invite_live/index.html.heex:3 #, elixir-autogen, elixir-format msgid "Invites" @@ -63,7 +63,7 @@ msgstr "" msgid "Reconnecting..." msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:34 +#: lib/lokal_web/controllers/user_registration_controller.ex:32 #, elixir-autogen, elixir-format msgid "Register" msgstr "" @@ -94,7 +94,7 @@ msgstr "" msgid "Disable" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:33 +#: lib/lokal_web/live/invite_live/index.ex:34 #, elixir-autogen, elixir-format msgid "Edit Invite" msgstr "" @@ -109,7 +109,7 @@ msgstr "" msgid "Name" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:37 +#: lib/lokal_web/live/invite_live/index.ex:38 #, elixir-autogen, elixir-format msgid "New Invite" msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index 4315849..3f7d04e 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -140,14 +140,15 @@ msgstr "" msgid "Reset password link is invalid or it has expired." msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:24 -#: lib/lokal_web/controllers/user_registration_controller.ex:55 +#: lib/lokal_web/controllers/user_registration_controller.ex:22 +#: lib/lokal_web/controllers/user_registration_controller.ex:51 #, elixir-autogen, elixir-format msgid "Sorry, public registration is disabled" msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:14 -#: lib/lokal_web/controllers/user_registration_controller.ex:45 +#: lib/lokal_web/controllers/user_registration_controller.ex:12 +#: lib/lokal_web/controllers/user_registration_controller.ex:41 +#: lib/lokal_web/controllers/user_registration_controller.ex:70 #, elixir-autogen, elixir-format msgid "Sorry, this invite was not found or expired" msgstr "" @@ -198,7 +199,7 @@ msgstr "" msgid "must have the @ sign and no spaces" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:18 +#: lib/lokal_web/live/invite_live/index.ex:19 #, elixir-autogen, elixir-format msgid "You are not authorized to view this page" msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/prompts.po b/priv/gettext/en/LC_MESSAGES/prompts.po index 615a925..ae78358 100644 --- a/priv/gettext/en/LC_MESSAGES/prompts.po +++ b/priv/gettext/en/LC_MESSAGES/prompts.po @@ -60,7 +60,7 @@ msgstr "" msgid "Password updated successfully." msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:73 +#: lib/lokal_web/controllers/user_registration_controller.ex:65 #, elixir-autogen, elixir-format msgid "Please check your email to verify your account" msgstr "" @@ -76,7 +76,7 @@ msgstr "" msgid "Are you sure you want to delete %{email}? This action is permanent!" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:127 +#: lib/lokal_web/live/invite_live/index.ex:128 #, elixir-autogen, elixir-format msgid "Copied to clipboard" msgstr "" @@ -96,37 +96,27 @@ msgstr "" msgid "Language updated successfully." msgstr "" -#: lib/lokal_web/live/invite_live/form_component.ex:62 -#, elixir-autogen, elixir-format -msgid "%{invite_name} created successfully" -msgstr "" - -#: lib/lokal_web/live/invite_live/index.ex:53 +#: lib/lokal_web/live/invite_live/index.ex:54 #, elixir-autogen, elixir-format msgid "%{invite_name} deleted succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:114 +#: lib/lokal_web/live/invite_live/index.ex:115 #, elixir-autogen, elixir-format msgid "%{invite_name} disabled succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:90 +#: lib/lokal_web/live/invite_live/index.ex:91 #, elixir-autogen, elixir-format msgid "%{invite_name} enabled succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:68 +#: lib/lokal_web/live/invite_live/index.ex:69 #, elixir-autogen, elixir-format msgid "%{invite_name} updated succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/form_component.ex:42 -#, elixir-autogen, elixir-format -msgid "%{invite_name} updated successfully" -msgstr "" - -#: lib/lokal_web/live/invite_live/index.ex:139 +#: lib/lokal_web/live/invite_live/index.ex:140 #, elixir-autogen, elixir-format msgid "%{user_email} deleted succesfully" msgstr "" @@ -145,3 +135,13 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "Register to setup Lokal" msgstr "" + +#: lib/lokal_web/live/invite_live/form_component.ex:80 +#, elixir-autogen, elixir-format, fuzzy +msgid "%{name} created successfully" +msgstr "" + +#: lib/lokal_web/live/invite_live/form_component.ex:62 +#, elixir-autogen, elixir-format, fuzzy +msgid "%{name} updated successfully" +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index 90049bd..7f657e7 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -137,14 +137,15 @@ msgstr "" msgid "Reset password link is invalid or it has expired." msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:24 -#: lib/lokal_web/controllers/user_registration_controller.ex:55 +#: lib/lokal_web/controllers/user_registration_controller.ex:22 +#: lib/lokal_web/controllers/user_registration_controller.ex:51 #, elixir-autogen, elixir-format msgid "Sorry, public registration is disabled" msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:14 -#: lib/lokal_web/controllers/user_registration_controller.ex:45 +#: lib/lokal_web/controllers/user_registration_controller.ex:12 +#: lib/lokal_web/controllers/user_registration_controller.ex:41 +#: lib/lokal_web/controllers/user_registration_controller.ex:70 #, elixir-autogen, elixir-format msgid "Sorry, this invite was not found or expired" msgstr "" @@ -195,7 +196,7 @@ msgstr "" msgid "must have the @ sign and no spaces" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:18 +#: lib/lokal_web/live/invite_live/index.ex:19 #, elixir-autogen, elixir-format msgid "You are not authorized to view this page" msgstr "" diff --git a/priv/gettext/prompts.pot b/priv/gettext/prompts.pot index af79a7a..0caf28d 100644 --- a/priv/gettext/prompts.pot +++ b/priv/gettext/prompts.pot @@ -60,7 +60,7 @@ msgstr "" msgid "Password updated successfully." msgstr "" -#: lib/lokal_web/controllers/user_registration_controller.ex:73 +#: lib/lokal_web/controllers/user_registration_controller.ex:65 #, elixir-autogen, elixir-format msgid "Please check your email to verify your account" msgstr "" @@ -76,7 +76,7 @@ msgstr "" msgid "Are you sure you want to delete %{email}? This action is permanent!" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:127 +#: lib/lokal_web/live/invite_live/index.ex:128 #, elixir-autogen, elixir-format msgid "Copied to clipboard" msgstr "" @@ -96,37 +96,27 @@ msgstr "" msgid "Language updated successfully." msgstr "" -#: lib/lokal_web/live/invite_live/form_component.ex:62 -#, elixir-autogen, elixir-format -msgid "%{invite_name} created successfully" -msgstr "" - -#: lib/lokal_web/live/invite_live/index.ex:53 +#: lib/lokal_web/live/invite_live/index.ex:54 #, elixir-autogen, elixir-format msgid "%{invite_name} deleted succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:114 +#: lib/lokal_web/live/invite_live/index.ex:115 #, elixir-autogen, elixir-format msgid "%{invite_name} disabled succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:90 +#: lib/lokal_web/live/invite_live/index.ex:91 #, elixir-autogen, elixir-format msgid "%{invite_name} enabled succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/index.ex:68 +#: lib/lokal_web/live/invite_live/index.ex:69 #, elixir-autogen, elixir-format msgid "%{invite_name} updated succesfully" msgstr "" -#: lib/lokal_web/live/invite_live/form_component.ex:42 -#, elixir-autogen, elixir-format -msgid "%{invite_name} updated successfully" -msgstr "" - -#: lib/lokal_web/live/invite_live/index.ex:139 +#: lib/lokal_web/live/invite_live/index.ex:140 #, elixir-autogen, elixir-format msgid "%{user_email} deleted succesfully" msgstr "" @@ -145,3 +135,13 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Register to setup Lokal" msgstr "" + +#: lib/lokal_web/live/invite_live/form_component.ex:80 +#, elixir-autogen, elixir-format +msgid "%{name} created successfully" +msgstr "" + +#: lib/lokal_web/live/invite_live/form_component.ex:62 +#, elixir-autogen, elixir-format +msgid "%{name} updated successfully" +msgstr "" diff --git a/test/lokal/accounts/invites_test.exs b/test/lokal/accounts/invites_test.exs new file mode 100644 index 0000000..d32020d --- /dev/null +++ b/test/lokal/accounts/invites_test.exs @@ -0,0 +1,122 @@ +defmodule Lokal.InvitesTest do + @moduledoc """ + This module tests the Lokal.Accounts.Invites context + """ + + use Lokal.DataCase + alias Ecto.Changeset + alias Lokal.Accounts.{Invite, Invites} + + @moduletag :invites_test + + @valid_attrs %{ + "name" => "some name", + "uses_left" => 10 + } + @update_attrs %{ + "name" => "some updated name", + "uses_left" => 5 + } + @invalid_attrs %{ + "name" => nil, + "token" => nil + } + + describe "invites" do + setup do + current_user = admin_fixture() + {:ok, invite} = Invites.create_invite(current_user, @valid_attrs) + [invite: invite, current_user: current_user] + end + + test "list_invites/0 returns all invites", %{invite: invite, current_user: current_user} do + assert Invites.list_invites(current_user) == [invite] + end + + test "get_invite!/1 returns the invite with given id", + %{invite: invite, current_user: current_user} do + assert Invites.get_invite!(invite.id, current_user) == invite + end + + test "valid_invite_token? returns for valid and invalid invite tokens", + %{invite: %{token: token}} do + refute Invites.valid_invite_token?(nil) + refute Invites.valid_invite_token?("") + assert Invites.valid_invite_token?(token) + end + + test "valid_invite_token? does not return true for a disabled invite by token", + %{invite: %{token: token} = invite, current_user: current_user} do + assert Invites.valid_invite_token?(token) + + {:ok, _invite} = Invites.update_invite(invite, %{uses_left: 1}, current_user) + {:ok, _invite} = Invites.use_invite(token) + + refute Invites.valid_invite_token?(token) + end + + test "use_invite/1 successfully uses an unlimited invite", + %{invite: %{token: token} = invite, current_user: current_user} do + {:ok, invite} = Invites.update_invite(invite, %{uses_left: nil}, current_user) + assert {:ok, ^invite} = Invites.use_invite(token) + assert {:ok, ^invite} = Invites.use_invite(token) + assert {:ok, ^invite} = Invites.use_invite(token) + end + + test "use_invite/1 successfully decrements an invite", %{invite: %{token: token}} do + assert {:ok, %{uses_left: 9}} = Invites.use_invite(token) + assert {:ok, %{uses_left: 8}} = Invites.use_invite(token) + assert {:ok, %{uses_left: 7}} = Invites.use_invite(token) + end + + test "use_invite/1 successfully disactivates an invite", + %{invite: %{token: token} = invite, current_user: current_user} do + {:ok, _invite} = Invites.update_invite(invite, %{uses_left: 1}, current_user) + assert {:ok, %{uses_left: 0, disabled_at: disabled_at}} = Invites.use_invite(token) + assert not is_nil(disabled_at) + end + + test "use_invite/1 does not work on disactivated invite", + %{invite: %{token: token} = invite, current_user: current_user} do + {:ok, _invite} = Invites.update_invite(invite, %{uses_left: 1}, current_user) + {:ok, _invite} = Invites.use_invite(token) + assert {:error, :invalid_token} = Invites.use_invite(token) + end + + test "create_invite/1 with valid data creates a invite", + %{current_user: current_user} do + assert {:ok, %Invite{} = invite} = Invites.create_invite(current_user, @valid_attrs) + assert invite.name == "some name" + end + + test "create_invite/1 with invalid data returns error changeset", + %{current_user: current_user} do + assert {:error, %Changeset{}} = Invites.create_invite(current_user, @invalid_attrs) + end + + test "update_invite/2 with valid data updates the invite", + %{invite: invite, current_user: current_user} do + assert {:ok, %Invite{} = new_invite} = + Invites.update_invite(invite, @update_attrs, current_user) + + assert new_invite.name == "some updated name" + assert new_invite.uses_left == 5 + end + + test "update_invite/2 with invalid data returns error changeset", + %{invite: invite, current_user: current_user} do + assert {:error, %Changeset{}} = Invites.update_invite(invite, @invalid_attrs, current_user) + assert invite == Invites.get_invite!(invite.id, current_user) + end + + test "delete_invite/1 deletes the invite", %{invite: invite, current_user: current_user} do + assert {:ok, %Invite{}} = Invites.delete_invite(invite, current_user) + assert_raise Ecto.NoResultsError, fn -> Invites.get_invite!(invite.id, current_user) end + end + + test "delete_invite!/1 deletes the invite", %{invite: invite, current_user: current_user} do + assert %Invite{} = Invites.delete_invite!(invite, current_user) + assert_raise Ecto.NoResultsError, fn -> Invites.get_invite!(invite.id, current_user) end + end + end +end diff --git a/test/lokal/invites_text.exs b/test/lokal/invites_text.exs deleted file mode 100644 index e27fdcd..0000000 --- a/test/lokal/invites_text.exs +++ /dev/null @@ -1,76 +0,0 @@ -defmodule Lokal.InvitesTest do - @moduledoc """ - This module tests the Invites context - """ - - use Lokal.DataCase - alias Ecto.Changeset - alias Lokal.{Invites, Invites.Invite} - - @moduletag :invites_test - - @valid_attrs %{ - "name" => "some name", - "token" => "some token" - } - @update_attrs %{ - "name" => "some updated name", - "token" => "some updated token" - } - @invalid_attrs %{ - "name" => nil, - "token" => nil - } - - describe "invites" do - setup do - current_user = admin_fixture() - {:ok, invite} = Invites.create_invite(current_user, @valid_attrs) - [invite: invite, current_user: current_user] - end - - test "list_invites/0 returns all invites", %{invite: invite, current_user: current_user} do - assert Invites.list_invites(current_user) == [invite] - end - - test "get_invite!/1 returns the invite with given id", - %{invite: invite, current_user: current_user} do - assert Invites.get_invite!(invite.id, current_user) == invite - end - - test "create_invite/1 with valid data creates a invite", - %{current_user: current_user} do - assert {:ok, %Invite{} = invite} = Invites.create_invite(current_user, @valid_attrs) - assert invite.name == "some name" - end - - test "create_invite/1 with invalid data returns error changeset", - %{current_user: current_user} do - assert {:error, %Changeset{}} = Invites.create_invite(current_user, @invalid_attrs) - end - - test "update_invite/2 with valid data updates the invite", - %{invite: invite, current_user: current_user} do - assert {:ok, %Invite{} = new_invite} = - Invites.update_invite(invite, @update_attrs, current_user) - - assert new_invite.name == "some updated name" - assert new_invite.token == new_invite.token - end - - test "update_invite/2 with invalid data returns error changeset", - %{invite: invite, current_user: current_user} do - assert {:error, %Changeset{}} = Invites.update_invite(invite, @invalid_attrs, current_user) - assert invite == Invites.get_invite!(invite.id, current_user) - end - - test "delete_invite/1 deletes the invite", %{invite: invite, current_user: current_user} do - assert {:ok, %Invite{}} = Invites.delete_invite(invite, current_user) - assert_raise Ecto.NoResultsError, fn -> Invites.get_invite!(invite.id, current_user) end - end - - test "change_invite/1 returns a invite changeset", %{invite: invite} do - assert %Changeset{} = Invites.change_invite(invite) - end - end -end diff --git a/test/lokal_web/live/invite_live_test.exs b/test/lokal_web/live/invite_live_test.exs index 262acb4..85c54a4 100644 --- a/test/lokal_web/live/invite_live_test.exs +++ b/test/lokal_web/live/invite_live_test.exs @@ -6,7 +6,7 @@ defmodule LokalWeb.InviteLiveTest do use LokalWeb.ConnCase import Phoenix.LiveViewTest import LokalWeb.Gettext - alias Lokal.Invites + alias Lokal.Accounts.Invites @moduletag :invite_live_test @create_attrs %{"name" => "some name"}