generating tokens with mix
authorAlex S <alex.strizhakov@gmail.com>
Sat, 6 Apr 2019 09:58:22 +0000 (16:58 +0700)
committerAlex S <alex.strizhakov@gmail.com>
Sat, 6 Apr 2019 09:58:22 +0000 (16:58 +0700)
lib/mix/tasks/pleroma/user.ex
lib/pleroma/user_invite_token.ex
priv/repo/migrations/20190404050946_add_fields_to_user_invite_tokens.exs [new file with mode: 0644]
test/tasks/user_test.exs

index 0d0bea8c08a7333aad1edff65ef3a7c118df4a33..00a9332920b51a7b6a38ca74931a9dcfe4520264 100644 (file)
@@ -7,6 +7,7 @@ defmodule Mix.Tasks.Pleroma.User do
   import Ecto.Changeset
   alias Mix.Tasks.Pleroma.Common
   alias Pleroma.User
+  alias Pleroma.UserInviteToken
 
   @shortdoc "Manages Pleroma users"
   @moduledoc """
@@ -26,7 +27,19 @@ defmodule Mix.Tasks.Pleroma.User do
 
   ## Generate an invite link.
 
-      mix pleroma.user invite
+      mix pleroma.user invite [OPTION...]
+
+    Options:
+    - `--expire_date DATE` - last day on which token is active (e.g. "2019-04-05")
+    - `--max_use NUMBER` - maximum numbers of token use
+
+  ## Generated invites list
+
+      mix pleroma.user invites_list
+
+  ## Revoke invite
+
+      mix pleroma.user invite_revoke TOKEN OR TOKEN_ID
 
   ## Delete the user's account.
 
@@ -287,11 +300,28 @@ defmodule Mix.Tasks.Pleroma.User do
     end
   end
 
-  def run(["invite"]) do
+  def run(["invite" | rest]) do
+    {options, [], []} =
+      OptionParser.parse(rest,
+        strict: [
+          expire_date: :string,
+          max_use: :integer
+        ]
+      )
+
+    expire_at =
+      with expire_date when expire_date != nil <- Keyword.get(options, :expire_date) do
+        Date.from_iso8601!(expire_date)
+      end
+
+    options = Keyword.put(options, :expire_at, expire_at)
+
     Common.start_pleroma()
 
-    with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
-      Mix.shell().info("Generated user invite token")
+    with {:ok, token} <- UserInviteToken.create_token(options) do
+      Mix.shell().info(
+        "Generated user invite token " <> String.replace(token.token_type, "_", " ")
+      )
 
       url =
         Pleroma.Web.Router.Helpers.redirect_url(
@@ -307,6 +337,43 @@ defmodule Mix.Tasks.Pleroma.User do
     end
   end
 
+  def run(["invites_list"]) do
+    Common.start_pleroma()
+
+    Mix.shell().info("Invites list:")
+
+    UserInviteToken.list_invites()
+    |> Enum.each(fn invite ->
+      expire_date =
+        case invite.expire_at do
+          nil -> nil
+          date -> " | Expire date: #{Date.to_string(date)}"
+        end
+
+      using_info =
+        case invite.max_use do
+          nil -> nil
+          max_use -> " | Max use: #{max_use}    Left use: #{max_use - invite.uses}"
+        end
+
+      Mix.shell().info(
+        "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.token_type} | Used: #{
+          invite.used
+        }#{expire_date}#{using_info}"
+      )
+    end)
+  end
+
+  def run(["invite_revoke", token]) do
+    Common.start_pleroma()
+
+    with {:ok, _} <- UserInviteToken.mark_as_used(token) do
+      Mix.shell().info("Invite for token #{token} was revoked.")
+    else
+      _ -> Mix.shell().error("No invite found with token #{token}")
+    end
+  end
+
   def run(["delete_activities", nickname]) do
     Common.start_pleroma()
 
index 9c5579934dddf6e68525992d1fcc7458e7da3997..3ed39ddd36ef414fa581a9e7cacf5abb7646f7b5 100644 (file)
@@ -6,34 +6,54 @@ defmodule Pleroma.UserInviteToken do
   use Ecto.Schema
 
   import Ecto.Changeset
-
+  import Ecto.Query
   alias Pleroma.Repo
   alias Pleroma.UserInviteToken
 
+  @type token :: String.t()
+
   schema "user_invite_tokens" do
     field(:token, :string)
     field(:used, :boolean, default: false)
+    field(:max_use, :integer)
+    field(:expire_at, :date)
+    field(:uses, :integer)
+    field(:token_type)
 
     timestamps()
   end
 
-  def create_token do
+  def create_token(options \\ []) do
     token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
 
-    token = %UserInviteToken{
-      used: false,
-      token: token
-    }
+    max_use = options[:max_use]
+    expire_at = options[:expire_at]
+
+    token =
+      %UserInviteToken{
+        used: false,
+        token: token,
+        max_use: max_use,
+        expire_at: expire_at,
+        uses: 0
+      }
+      |> token_type()
 
     Repo.insert(token)
   end
 
+  def list_invites do
+    query = from(u in UserInviteToken, order_by: u.id)
+    Repo.all(query)
+  end
+
   def used_changeset(struct) do
     struct
     |> cast(%{}, [])
     |> put_change(:used, true)
   end
 
+  @spec mark_as_used(token()) :: {:ok, UserInviteToken.t()} | {:error, token()}
   def mark_as_used(token) do
     with %{used: false} = token <- Repo.get_by(UserInviteToken, %{token: token}),
          {:ok, token} <- Repo.update(used_changeset(token)) do
@@ -42,4 +62,61 @@ defmodule Pleroma.UserInviteToken do
       _e -> {:error, token}
     end
   end
+
+  defp token_type(%{expire_at: nil, max_use: nil} = token), do: %{token | token_type: "one_time"}
+
+  defp token_type(%{expire_at: _expire_at, max_use: nil} = token),
+    do: %{token | token_type: "date_limited"}
+
+  defp token_type(%{expire_at: nil, max_use: _max_use} = token),
+    do: %{token | token_type: "reusable"}
+
+  defp token_type(%{expire_at: _expire_at, max_use: _max_use} = token),
+    do: %{token | token_type: "reusable_date_limited"}
+
+  @spec valid_token?(UserInviteToken.t()) :: boolean()
+  def valid_token?(%{token_type: "one_time"} = token) do
+    not token.used
+  end
+
+  def valid_token?(%{token_type: "date_limited"} = token) do
+    not_overdue_date?(token) and not token.used
+  end
+
+  def valid_token?(%{token_type: "reusable"} = token) do
+    token.uses < token.max_use and not token.used
+  end
+
+  def valid_token?(%{token_type: "reusable_date_limited"} = token) do
+    not_overdue_date?(token) and token.uses < token.max_use and not token.used
+  end
+
+  defp not_overdue_date?(%{expire_at: expire_at} = token) do
+    Date.compare(Date.utc_today(), expire_at) in [:lt, :eq] ||
+      (Repo.update!(change(token, used: true)) && false)
+  end
+
+  def update_usage(%{token_type: "date_limited"}), do: nil
+
+  def update_usage(%{token_type: "one_time"} = token) do
+    UserInviteToken.mark_as_used(token.token)
+  end
+
+  def update_usage(%{token_type: token_type} = token)
+      when token_type == "reusable" or token_type == "reusable_date_limited" do
+    new_uses = token.uses + 1
+
+    changes = %{
+      uses: new_uses
+    }
+
+    changes =
+      if new_uses >= token.max_use do
+        Map.put(changes, :used, true)
+      else
+        changes
+      end
+
+    change(token, changes) |> Repo.update!()
+  end
 end
diff --git a/priv/repo/migrations/20190404050946_add_fields_to_user_invite_tokens.exs b/priv/repo/migrations/20190404050946_add_fields_to_user_invite_tokens.exs
new file mode 100644 (file)
index 0000000..abdd5e2
--- /dev/null
@@ -0,0 +1,12 @@
+defmodule Pleroma.Repo.Migrations.AddFieldsToUserInviteTokens do
+  use Ecto.Migration
+
+  def change do
+    alter table(:user_invite_tokens) do
+      add(:expire_at, :date)
+      add(:uses, :integer, default: 0)
+      add(:max_use, :integer)
+      add(:token_type, :string, default: "one_time")
+    end
+  end
+end
index 1030bd555ef2bd154914899c1ee6ec8ccb7aa300..c55711b0446596429e852fdaa579175453f0e76b 100644 (file)
@@ -245,7 +245,86 @@ defmodule Mix.Tasks.Pleroma.UserTest do
              end) =~ "http"
 
       assert_received {:mix_shell, :info, [message]}
-      assert message =~ "Generated"
+      assert message =~ "Generated user invite token one time"
+    end
+
+    test "token is generated with expire_at" do
+      assert capture_io(fn ->
+               Mix.Tasks.Pleroma.User.run([
+                 "invite",
+                 "--expire-date",
+                 Date.to_string(Date.utc_today())
+               ])
+             end)
+
+      assert_received {:mix_shell, :info, [message]}
+      assert message =~ "Generated user invite token date limited"
+    end
+
+    test "token is generated with max use" do
+      assert capture_io(fn ->
+               Mix.Tasks.Pleroma.User.run([
+                 "invite",
+                 "--max-use",
+                 "5"
+               ])
+             end)
+
+      assert_received {:mix_shell, :info, [message]}
+      assert message =~ "Generated user invite token reusable"
+    end
+
+    test "token is generated with max use and expire date" do
+      assert capture_io(fn ->
+               Mix.Tasks.Pleroma.User.run([
+                 "invite",
+                 "--max-use",
+                 "5",
+                 "--expire-date",
+                 Date.to_string(Date.utc_today())
+               ])
+             end)
+
+      assert_received {:mix_shell, :info, [message]}
+      assert message =~ "Generated user invite token reusable date limited"
+    end
+  end
+
+  describe "running invites_list" do
+    test "invites are listed" do
+      {:ok, invite} = Pleroma.UserInviteToken.create_token()
+
+      {:ok, invite2} =
+        Pleroma.UserInviteToken.create_token(expire_at: Date.utc_today(), max_use: 15)
+
+      assert capture_io(fn ->
+               Mix.Tasks.Pleroma.User.run([
+                 "invites_list"
+               ])
+             end)
+
+      assert_received {:mix_shell, :info, [message]}
+      assert_received {:mix_shell, :info, [message2]}
+      assert_received {:mix_shell, :info, [message3]}
+      assert message =~ "Invites list:"
+      assert message2 =~ invite.token_type
+      assert message3 =~ invite2.token_type
+    end
+  end
+
+  describe "running invite revoke" do
+    test "invite is revoked" do
+      {:ok, invite} = Pleroma.UserInviteToken.create_token(expire_at: Date.utc_today())
+
+      assert capture_io(fn ->
+               Mix.Tasks.Pleroma.User.run([
+                 "invite_revoke",
+                 invite.token
+               ])
+             end)
+
+      assert_received {:mix_shell, :info, [message]}
+      assert message =~ "Invite for token #{invite.token} was revoked."
     end
   end