record invites
This commit is contained in:
		| @@ -124,16 +124,15 @@ defmodule Lokal.Accounts do | ||||
|     |> 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} | ||||
|         {:ok, nil} | ||||
|       else | ||||
|         Invites.use_invite(invite_token) | ||||
|       end | ||||
|     end) | ||||
|     |> Multi.insert(:add_user, fn %{users_count: count} -> | ||||
|     |> Multi.insert(:add_user, fn %{users_count: count, use_invite: invite} -> | ||||
|       # if no registered users, make first user an admin | ||||
|       role = if count == 0, do: :admin, else: :user | ||||
|  | ||||
|       User.registration_changeset(attrs) |> User.role_changeset(role) | ||||
|       User.registration_changeset(attrs, invite) |> User.role_changeset(role) | ||||
|     end) | ||||
|     |> Repo.transaction() | ||||
|     |> case do | ||||
| @@ -158,7 +157,7 @@ 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) | ||||
|     User.registration_changeset(attrs, nil, hash_password: false) | ||||
|   end | ||||
|  | ||||
|   ## Settings | ||||
|   | ||||
| @@ -7,7 +7,7 @@ defmodule Lokal.Accounts.Invite do | ||||
|  | ||||
|   use Ecto.Schema | ||||
|   import Ecto.Changeset | ||||
|   alias Ecto.{Changeset, UUID} | ||||
|   alias Ecto.{Association, Changeset, UUID} | ||||
|   alias Lokal.Accounts.User | ||||
|  | ||||
|   @primary_key {:id, :binary_id, autogenerate: true} | ||||
| @@ -18,7 +18,9 @@ defmodule Lokal.Accounts.Invite do | ||||
|     field :uses_left, :integer, default: nil | ||||
|     field :disabled_at, :naive_datetime | ||||
|  | ||||
|     belongs_to :user, User | ||||
|     belongs_to :created_by, User | ||||
|  | ||||
|     has_many :users, User | ||||
|  | ||||
|     timestamps() | ||||
|   end | ||||
| @@ -29,8 +31,9 @@ defmodule Lokal.Accounts.Invite do | ||||
|           token: token(), | ||||
|           uses_left: integer() | nil, | ||||
|           disabled_at: NaiveDateTime.t(), | ||||
|           user: User.t(), | ||||
|           user_id: User.id(), | ||||
|           created_by: User.t() | nil | Association.NotLoaded.t(), | ||||
|           created_by_id: User.id() | nil, | ||||
|           users: [User.t()] | Association.NotLoaded.t(), | ||||
|           inserted_at: NaiveDateTime.t(), | ||||
|           updated_at: NaiveDateTime.t() | ||||
|         } | ||||
| @@ -43,9 +46,9 @@ defmodule Lokal.Accounts.Invite do | ||||
|   @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) | ||||
|     |> change(token: token, created_by_id: user_id) | ||||
|     |> cast(attrs, [:name, :uses_left, :disabled_at]) | ||||
|     |> validate_required([:name, :token, :user_id]) | ||||
|     |> validate_required([:name, :token, :created_by_id]) | ||||
|     |> validate_number(:uses_left, greater_than_or_equal_to: 0) | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -98,6 +98,15 @@ defmodule Lokal.Accounts.Invites do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @spec get_use_count(Invite.t(), User.t()) :: non_neg_integer() | ||||
|   def get_use_count(%Invite{id: invite_id}, %User{role: :admin}) do | ||||
|     Repo.one( | ||||
|       from u in User, | ||||
|         where: u.invite_id == ^invite_id, | ||||
|         select: count(u.id) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   @spec decrement_invite_changeset(Invite.t()) :: Invite.changeset() | ||||
|   defp decrement_invite_changeset(%Invite{uses_left: nil} = invite) do | ||||
|     invite |> Invite.update_changeset(%{}) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ defmodule Lokal.Accounts.User do | ||||
|   use Ecto.Schema | ||||
|   import Ecto.Changeset | ||||
|   import LokalWeb.Gettext | ||||
|   alias Ecto.{Changeset, UUID} | ||||
|   alias Ecto.{Association, Changeset, UUID} | ||||
|   alias Lokal.Accounts.{Invite, User} | ||||
|  | ||||
|   @derive {Jason.Encoder, | ||||
| @@ -28,7 +28,9 @@ defmodule Lokal.Accounts.User do | ||||
|     field :role, Ecto.Enum, values: [:admin, :user], default: :user | ||||
|     field :locale, :string | ||||
|  | ||||
|     has_many :invites, Invite, on_delete: :delete_all | ||||
|     has_many :created_invites, Invite, foreign_key: :created_by_id | ||||
|  | ||||
|     belongs_to :invite, Invite | ||||
|  | ||||
|     timestamps() | ||||
|   end | ||||
| @@ -41,7 +43,9 @@ defmodule Lokal.Accounts.User do | ||||
|           confirmed_at: NaiveDateTime.t(), | ||||
|           role: role(), | ||||
|           locale: String.t() | nil, | ||||
|           invites: [Invite.t()], | ||||
|           created_invites: [Invite.t()] | Association.NotLoaded.t(), | ||||
|           invite: Invite.t() | nil | Association.NotLoaded.t(), | ||||
|           invite_id: Invite.id() | nil, | ||||
|           inserted_at: NaiveDateTime.t(), | ||||
|           updated_at: NaiveDateTime.t() | ||||
|         } | ||||
| @@ -67,11 +71,12 @@ defmodule Lokal.Accounts.User do | ||||
|       validations on a LiveView form), this option can be set to `false`. | ||||
|       Defaults to `true`. | ||||
|   """ | ||||
|   @spec registration_changeset(attrs :: map()) :: changeset() | ||||
|   @spec registration_changeset(attrs :: map(), opts :: keyword()) :: changeset() | ||||
|   def registration_changeset(attrs, opts \\ []) do | ||||
|   @spec registration_changeset(attrs :: map(), Invite.t() | nil) :: changeset() | ||||
|   @spec registration_changeset(attrs :: map(), Invite.t() | nil, opts :: keyword()) :: changeset() | ||||
|   def registration_changeset(attrs, invite, opts \\ []) do | ||||
|     %User{} | ||||
|     |> cast(attrs, [:email, :password, :locale]) | ||||
|     |> put_change(:invite_id, if(invite, do: invite.id)) | ||||
|     |> validate_email() | ||||
|     |> validate_password(opts) | ||||
|   end | ||||
|   | ||||
| @@ -4,10 +4,19 @@ defmodule LokalWeb.Components.InviteCard do | ||||
|   """ | ||||
|  | ||||
|   use LokalWeb, :component | ||||
|   alias Lokal.Accounts.{Invite, Invites, User} | ||||
|   alias LokalWeb.Endpoint | ||||
|  | ||||
|   def invite_card(assigns) do | ||||
|     assigns = assigns |> assign_new(:code_actions, fn -> [] end) | ||||
|   attr :invite, Invite, required: true | ||||
|   attr :current_user, User, required: true | ||||
|   slot(:inner_block) | ||||
|   slot(:code_actions) | ||||
|  | ||||
|   def invite_card(%{invite: invite, current_user: current_user} = assigns) do | ||||
|     assigns = | ||||
|       assigns | ||||
|       |> assign(:use_count, Invites.get_use_count(invite, current_user)) | ||||
|       |> assign_new(:code_actions, fn -> [] end) | ||||
|  | ||||
|     ~H""" | ||||
|     <div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4 | ||||
| @@ -21,8 +30,8 @@ defmodule LokalWeb.Components.InviteCard do | ||||
|         <h2 class="title text-md"> | ||||
|           <%= if @invite.uses_left do %> | ||||
|             <%= gettext( | ||||
|               "Uses Left: %{uses_left}", | ||||
|               uses_left: @invite.uses_left | ||||
|               "Uses Left: %{uses_left_count}", | ||||
|               uses_left_count: @invite.uses_left | ||||
|             ) %> | ||||
|           <% else %> | ||||
|             <%= gettext("Uses Left: Unlimited") %> | ||||
| @@ -39,6 +48,10 @@ defmodule LokalWeb.Components.InviteCard do | ||||
|         filename={@invite.name} | ||||
|       /> | ||||
|  | ||||
|       <h2 :if={@use_count != 0} class="title text-md"> | ||||
|         <%= gettext("Uses: %{uses_count}", uses_count: @use_count) %> | ||||
|       </h2> | ||||
|  | ||||
|       <div class="flex flex-row flex-wrap justify-center items-center"> | ||||
|         <code | ||||
|           id={"code-#{@invite.id}"} | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
|   <% end %> | ||||
|  | ||||
|   <div class="w-full flex flex-row flex-wrap justify-center items-center"> | ||||
|     <.invite_card :for={invite <- @invites} invite={invite}> | ||||
|     <.invite_card :for={invite <- @invites} invite={invite} current_user={@current_user}> | ||||
|       <:code_actions> | ||||
|         <form phx-submit="copy_to_clipboard"> | ||||
|           <button | ||||
|   | ||||
| @@ -31,7 +31,7 @@ msgstr "" | ||||
| msgid "Forgot your password?" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:33 | ||||
| #: lib/lokal_web/components/invite_card.ex:42 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Invite Disabled" | ||||
| msgstr "" | ||||
| @@ -169,12 +169,7 @@ msgstr "" | ||||
| msgid "User was confirmed at%{confirmed_datetime}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:23 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Uses Left: %{uses_left}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:28 | ||||
| #: lib/lokal_web/components/invite_card.ex:37 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Uses Left: Unlimited" | ||||
| msgstr "" | ||||
| @@ -278,3 +273,13 @@ msgstr "" | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Lokal | %{title}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:32 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Uses Left: %{uses_left_count}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:52 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Uses: %{uses_count}" | ||||
| msgstr "" | ||||
|   | ||||
| @@ -31,7 +31,7 @@ msgstr "" | ||||
| msgid "Forgot your password?" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:33 | ||||
| #: lib/lokal_web/components/invite_card.ex:42 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Invite Disabled" | ||||
| msgstr "" | ||||
| @@ -169,12 +169,7 @@ msgstr "" | ||||
| msgid "User was confirmed at%{confirmed_datetime}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:23 | ||||
| #, elixir-autogen, elixir-format, fuzzy | ||||
| msgid "Uses Left: %{uses_left}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:28 | ||||
| #: lib/lokal_web/components/invite_card.ex:37 | ||||
| #, elixir-autogen, elixir-format, fuzzy | ||||
| msgid "Uses Left: Unlimited" | ||||
| msgstr "" | ||||
| @@ -278,3 +273,13 @@ msgstr "" | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Lokal | %{title}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:32 | ||||
| #, elixir-autogen, elixir-format, fuzzy | ||||
| msgid "Uses Left: %{uses_left_count}" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal_web/components/invite_card.ex:52 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "Uses: %{uses_count}" | ||||
| msgstr "" | ||||
|   | ||||
| @@ -179,22 +179,22 @@ msgstr "" | ||||
| msgid "You must confirm your account and log in to access this page." | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:137 | ||||
| #: lib/lokal/accounts/user.ex:142 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "did not change" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:158 | ||||
| #: lib/lokal/accounts/user.ex:163 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "does not match password" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:195 | ||||
| #: lib/lokal/accounts/user.ex:200 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "is not valid" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:92 | ||||
| #: lib/lokal/accounts/user.ex:97 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "must have the @ sign and no spaces" | ||||
| msgstr "" | ||||
|   | ||||
| @@ -176,22 +176,22 @@ msgstr "" | ||||
| msgid "You must confirm your account and log in to access this page." | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:137 | ||||
| #: lib/lokal/accounts/user.ex:142 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "did not change" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:158 | ||||
| #: lib/lokal/accounts/user.ex:163 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "does not match password" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:195 | ||||
| #: lib/lokal/accounts/user.ex:200 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "is not valid" | ||||
| msgstr "" | ||||
|  | ||||
| #: lib/lokal/accounts/user.ex:92 | ||||
| #: lib/lokal/accounts/user.ex:97 | ||||
| #, elixir-autogen, elixir-format | ||||
| msgid "must have the @ sign and no spaces" | ||||
| msgstr "" | ||||
|   | ||||
							
								
								
									
										11
									
								
								priv/repo/migrations/20230204191547_record_invites.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								priv/repo/migrations/20230204191547_record_invites.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| defmodule Lokal.Repo.Migrations.RecordInvites do | ||||
|   use Ecto.Migration | ||||
|  | ||||
|   def change do | ||||
|     alter table(:users) do | ||||
|       add :invite_id, references(:invites, type: :binary_id) | ||||
|     end | ||||
|  | ||||
|     rename table(:invites), :user_id, to: :created_by_id | ||||
|   end | ||||
| end | ||||
| @@ -5,17 +5,13 @@ defmodule Lokal.InvitesTest do | ||||
|  | ||||
|   use Lokal.DataCase | ||||
|   alias Ecto.Changeset | ||||
|   alias Lokal.Accounts | ||||
|   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 | ||||
|     "name" => "some name" | ||||
|   } | ||||
|   @invalid_attrs %{ | ||||
|     "name" => nil, | ||||
| @@ -55,6 +51,27 @@ defmodule Lokal.InvitesTest do | ||||
|       refute Invites.valid_invite_token?(token) | ||||
|     end | ||||
|  | ||||
|     test "get_use_count/2 returns the correct invite usage", | ||||
|          %{invite: %{token: token} = invite, current_user: current_user} do | ||||
|       assert 0 == Invites.get_use_count(invite, current_user) | ||||
|  | ||||
|       assert {:ok, _user} = | ||||
|                Accounts.register_user( | ||||
|                  %{"email" => unique_user_email(), "password" => valid_user_password()}, | ||||
|                  token | ||||
|                ) | ||||
|  | ||||
|       assert 1 == Invites.get_use_count(invite, current_user) | ||||
|  | ||||
|       assert {:ok, _user} = | ||||
|                Accounts.register_user( | ||||
|                  %{"email" => unique_user_email(), "password" => valid_user_password()}, | ||||
|                  token | ||||
|                ) | ||||
|  | ||||
|       assert 2 == Invites.get_use_count(invite, current_user) | ||||
|     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) | ||||
| @@ -63,7 +80,9 @@ defmodule Lokal.InvitesTest do | ||||
|       assert {:ok, ^invite} = Invites.use_invite(token) | ||||
|     end | ||||
|  | ||||
|     test "use_invite/1 successfully decrements an invite", %{invite: %{token: token}} do | ||||
|     test "use_invite/1 successfully decrements an invite", | ||||
|          %{invite: %{token: token} = invite, current_user: current_user} do | ||||
|       {:ok, _invite} = Invites.update_invite(invite, %{uses_left: 10}, current_user) | ||||
|       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) | ||||
| @@ -83,26 +102,61 @@ defmodule Lokal.InvitesTest do | ||||
|       assert {:error, :invalid_token} = Invites.use_invite(token) | ||||
|     end | ||||
|  | ||||
|     test "create_invite/1 with valid data creates a invite", | ||||
|     test "create_invite/1 with valid data creates an unlimited invite", | ||||
|          %{current_user: current_user} do | ||||
|       assert {:ok, %Invite{} = invite} = Invites.create_invite(current_user, @valid_attrs) | ||||
|       assert {:ok, %Invite{} = invite} = | ||||
|                Invites.create_invite(current_user, %{ | ||||
|                  "name" => "some name" | ||||
|                }) | ||||
|  | ||||
|       assert invite.name == "some name" | ||||
|     end | ||||
|  | ||||
|     test "create_invite/1 with valid data creates a limited invite", | ||||
|          %{current_user: current_user} do | ||||
|       assert {:ok, %Invite{} = invite} = | ||||
|                Invites.create_invite(current_user, %{ | ||||
|                  "name" => "some name", | ||||
|                  "uses_left" => 10 | ||||
|                }) | ||||
|  | ||||
|       assert invite.name == "some name" | ||||
|       assert invite.uses_left == 10 | ||||
|     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", | ||||
|     test "update_invite/2 can set an invite to be limited", | ||||
|          %{invite: invite, current_user: current_user} do | ||||
|       assert {:ok, %Invite{} = new_invite} = | ||||
|                Invites.update_invite(invite, @update_attrs, current_user) | ||||
|                Invites.update_invite( | ||||
|                  invite, | ||||
|                  %{"name" => "some updated name", "uses_left" => 5}, | ||||
|                  current_user | ||||
|                ) | ||||
|  | ||||
|       assert new_invite.name == "some updated name" | ||||
|       assert new_invite.uses_left == 5 | ||||
|     end | ||||
|  | ||||
|     test "update_invite/2 can set an invite to be unlimited", | ||||
|          %{invite: invite, current_user: current_user} do | ||||
|       {:ok, invite} = Invites.update_invite(invite, %{"uses_left" => 5}, current_user) | ||||
|  | ||||
|       assert {:ok, %Invite{} = new_invite} = | ||||
|                Invites.update_invite( | ||||
|                  invite, | ||||
|                  %{"name" => "some updated name", "uses_left" => nil}, | ||||
|                  current_user | ||||
|                ) | ||||
|  | ||||
|       assert new_invite.name == "some updated name" | ||||
|       assert new_invite.uses_left |> is_nil() | ||||
|     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) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ defmodule Lokal.AccountsTest do | ||||
|   use Lokal.DataCase | ||||
|   alias Ecto.Changeset | ||||
|   alias Lokal.Accounts | ||||
|   alias Lokal.Accounts.{User, UserToken} | ||||
|   alias Lokal.Accounts.{Invites, User, UserToken} | ||||
|  | ||||
|   @moduletag :accounts_test | ||||
|  | ||||
| @@ -102,6 +102,17 @@ defmodule Lokal.AccountsTest do | ||||
|       assert is_nil(user.confirmed_at) | ||||
|       assert is_nil(user.password) | ||||
|     end | ||||
|  | ||||
|     test "records used invite during registration" do | ||||
|       {:ok, %{id: invite_id, token: token}} = | ||||
|         admin_fixture() |> Invites.create_invite(%{"name" => "my invite"}) | ||||
|  | ||||
|       assert {:ok, %{invite_id: ^invite_id}} = | ||||
|                Accounts.register_user( | ||||
|                  %{"email" => unique_user_email(), "password" => valid_user_password()}, | ||||
|                  token | ||||
|                ) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe "change_user_registration/2" do | ||||
|   | ||||
		Reference in New Issue
	
	Block a user