diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/mix/tasks/pleroma/user.ex | 83 | ||||
-rw-r--r-- | lib/pleroma/user_invite_token.ex | 113 | ||||
-rw-r--r-- | lib/pleroma/web/admin_api/admin_api_controller.ex | 27 | ||||
-rw-r--r-- | lib/pleroma/web/admin_api/views/account_view.ex | 18 | ||||
-rw-r--r-- | lib/pleroma/web/router.ex | 2 | ||||
-rw-r--r-- | lib/pleroma/web/twitter_api/twitter_api.ex | 61 |
6 files changed, 252 insertions, 52 deletions
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 0d0bea8c0..441168df2 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -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: + - `--expires_at DATE` - last day on which token is active (e.g. "2019-04-05") + - `--max_use NUMBER` - maximum numbers of token uses + + ## List generated invites + + mix pleroma.user invites + + ## Revoke invite + + mix pleroma.user revoke_invite TOKEN OR TOKEN_ID ## Delete the user's account. @@ -287,23 +300,79 @@ defmodule Mix.Tasks.Pleroma.User do end end - def run(["invite"]) do + def run(["invite" | rest]) do + {options, [], []} = + OptionParser.parse(rest, + strict: [ + expires_at: :string, + max_use: :integer + ] + ) + + options = + options + |> Keyword.update(:expires_at, {:ok, nil}, fn + nil -> {:ok, nil} + val -> Date.from_iso8601(val) + end) + |> Enum.into(%{}) + Common.start_pleroma() - with {:ok, token} <- Pleroma.UserInviteToken.create_token() do - Mix.shell().info("Generated user invite token") + with {:ok, val} <- options[:expires_at], + options = Map.put(options, :expires_at, val), + {:ok, invite} <- UserInviteToken.create_invite(options) do + Mix.shell().info( + "Generated user invite token " <> String.replace(invite.invite_type, "_", " ") + ) url = Pleroma.Web.Router.Helpers.redirect_url( Pleroma.Web.Endpoint, :registration_page, - token.token + invite.token ) IO.puts(url) else - _ -> - Mix.shell().error("Could not create invite token.") + error -> + Mix.shell().error("Could not create invite token: #{inspect(error)}") + end + end + + def run(["invites"]) do + Common.start_pleroma() + + Mix.shell().info("Invites list:") + + UserInviteToken.list_invites() + |> Enum.each(fn invite -> + expire_info = + with expires_at when not is_nil(expires_at) <- invite.expires_at do + " | Expires at: #{Date.to_string(expires_at)}" + end + + using_info = + with max_use when not is_nil(max_use) <- invite.max_use do + " | Max use: #{max_use} Left use: #{max_use - invite.uses}" + end + + Mix.shell().info( + "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{ + invite.used + }#{expire_info}#{using_info}" + ) + end) + end + + def run(["revoke_invite", token]) do + Common.start_pleroma() + + with {:ok, invite} <- UserInviteToken.find_by_token(token), + {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do + Mix.shell().info("Invite for token #{token} was revoked.") + else + _ -> Mix.shell().error("No invite found with token #{token}") end end diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex index 9c5579934..86f0a5486 100644 --- a/lib/pleroma/user_invite_token.ex +++ b/lib/pleroma/user_invite_token.ex @@ -6,40 +6,119 @@ defmodule Pleroma.UserInviteToken do use Ecto.Schema import Ecto.Changeset - + import Ecto.Query alias Pleroma.Repo alias Pleroma.UserInviteToken + @type t :: %__MODULE__{} + @type token :: String.t() + schema "user_invite_tokens" do field(:token, :string) field(:used, :boolean, default: false) + field(:max_use, :integer) + field(:expires_at, :date) + field(:uses, :integer, default: 0) + field(:invite_type, :string) timestamps() end - def create_token do + @spec create_invite(map()) :: UserInviteToken.t() + def create_invite(params \\ %{}) do + %UserInviteToken{} + |> cast(params, [:max_use, :expires_at]) + |> add_token() + |> assign_type() + |> Repo.insert() + end + + defp add_token(changeset) do token = :crypto.strong_rand_bytes(32) |> Base.url_encode64() + put_change(changeset, :token, token) + end - token = %UserInviteToken{ - used: false, - token: token - } + defp assign_type(%{changes: %{max_use: _max_use, expires_at: _expires_at}} = changeset) do + put_change(changeset, :invite_type, "reusable_date_limited") + end + + defp assign_type(%{changes: %{expires_at: _expires_at}} = changeset) do + put_change(changeset, :invite_type, "date_limited") + end + + defp assign_type(%{changes: %{max_use: _max_use}} = changeset) do + put_change(changeset, :invite_type, "reusable") + end + + defp assign_type(changeset), do: put_change(changeset, :invite_type, "one_time") - Repo.insert(token) + @spec list_invites() :: [UserInviteToken.t()] + 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) + @spec update_invite!(UserInviteToken.t(), map()) :: UserInviteToken.t() | no_return() + def update_invite!(invite, changes) do + change(invite, changes) |> Repo.update!() end - def mark_as_used(token) do - with %{used: false} = token <- Repo.get_by(UserInviteToken, %{token: token}), - {:ok, token} <- Repo.update(used_changeset(token)) do - {:ok, token} - else - _e -> {:error, token} + @spec update_invite(UserInviteToken.t(), map()) :: + {:ok, UserInviteToken.t()} | {:error, Changeset.t()} + def update_invite(invite, changes) do + change(invite, changes) |> Repo.update() + end + + @spec find_by_token!(token()) :: UserInviteToken.t() | no_return() + def find_by_token!(token), do: Repo.get_by!(UserInviteToken, token: token) + + @spec find_by_token(token()) :: {:ok, UserInviteToken.t()} | nil + def find_by_token(token) do + with invite <- Repo.get_by(UserInviteToken, token: token) do + {:ok, invite} end end + + @spec valid_invite?(UserInviteToken.t()) :: boolean() + def valid_invite?(%{invite_type: "one_time"} = invite) do + not invite.used + end + + def valid_invite?(%{invite_type: "date_limited"} = invite) do + not_overdue_date?(invite) and not invite.used + end + + def valid_invite?(%{invite_type: "reusable"} = invite) do + invite.uses < invite.max_use and not invite.used + end + + def valid_invite?(%{invite_type: "reusable_date_limited"} = invite) do + not_overdue_date?(invite) and invite.uses < invite.max_use and not invite.used + end + + defp not_overdue_date?(%{expires_at: expires_at}) do + Date.compare(Date.utc_today(), expires_at) in [:lt, :eq] + end + + @spec update_usage!(UserInviteToken.t()) :: nil | UserInviteToken.t() | no_return() + def update_usage!(%{invite_type: "date_limited"}), do: nil + + def update_usage!(%{invite_type: "one_time"} = invite), + do: update_invite!(invite, %{used: true}) + + def update_usage!(%{invite_type: invite_type} = invite) + when invite_type == "reusable" or invite_type == "reusable_date_limited" do + changes = %{ + uses: invite.uses + 1 + } + + changes = + if changes.uses >= invite.max_use do + Map.put(changes, :used, true) + else + changes + end + + update_invite!(invite, changes) + end end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 78bf31893..70a5b5c5d 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do use Pleroma.Web, :controller alias Pleroma.User + alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.Search @@ -235,7 +236,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do with true <- Pleroma.Config.get([:instance, :invites_enabled]) && !Pleroma.Config.get([:instance, :registrations_open]), - {:ok, invite_token} <- Pleroma.UserInviteToken.create_token(), + {:ok, invite_token} <- UserInviteToken.create_invite(), email <- Pleroma.UserEmail.user_invitation_email(user, invite_token, email, params["name"]), {:ok, _} <- Pleroma.Mailer.deliver(email) do @@ -244,11 +245,29 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end @doc "Get a account registeration invite token (base64 string)" - def get_invite_token(conn, _params) do - {:ok, token} = Pleroma.UserInviteToken.create_token() + def get_invite_token(conn, params) do + options = params["invite"] || %{} + {:ok, invite} = UserInviteToken.create_invite(options) conn - |> json(token.token) + |> json(invite.token) + end + + @doc "Get list of created invites" + def invites(conn, _params) do + invites = UserInviteToken.list_invites() + + conn + |> json(AccountView.render("invites.json", %{invites: invites})) + end + + @doc "Revokes invite by token" + def revoke_invite(conn, %{"token" => token}) do + invite = UserInviteToken.find_by_token!(token) + {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) + + conn + |> json(AccountView.render("invite.json", %{invite: updated_invite})) end @doc "Get a password reset token (base64 string) for given nickname" diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 4d6f921ef..28bb667d8 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -26,4 +26,22 @@ defmodule Pleroma.Web.AdminAPI.AccountView do "tags" => user.tags || [] } end + + def render("invite.json", %{invite: invite}) do + %{ + "id" => invite.id, + "token" => invite.token, + "used" => invite.used, + "expires_at" => invite.expires_at, + "uses" => invite.uses, + "max_use" => invite.max_use, + "invite_type" => invite.invite_type + } + end + + def render("invites.json", %{invites: invites}) do + %{ + invites: render_many(invites, AccountView, "invite.json", as: :invite) + } + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 06c8d7a03..172f337db 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -168,6 +168,8 @@ defmodule Pleroma.Web.Router do delete("/relay", AdminAPIController, :relay_unfollow) get("/invite_token", AdminAPIController, :get_invite_token) + get("/invites", AdminAPIController, :invites) + post("/revoke_invite", AdminAPIController, :revoke_invite) post("/email_invite", AdminAPIController, :email_invite) get("/password_reset", AdminAPIController, :get_password_reset) diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 9b081a316..9e9a46cf1 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -129,7 +129,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do end def register_user(params) do - token_string = params["token"] + token = params["token"] params = %{ nickname: params["nickname"], @@ -163,36 +163,49 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do {:error, %{error: Jason.encode!(%{captcha: [error]})}} else registrations_open = Pleroma.Config.get([:instance, :registrations_open]) + registration_process(registrations_open, params, token) + end + end - # no need to query DB if registration is open - token = - unless registrations_open || is_nil(token_string) do - Repo.get_by(UserInviteToken, %{token: token_string}) - end + defp registration_process(registration_open, params, token) + when registration_open == false or is_nil(registration_open) do + invite = + unless is_nil(token) do + Repo.get_by(UserInviteToken, %{token: token}) + end - cond do - registrations_open || (!is_nil(token) && !token.used) -> - changeset = User.register_changeset(%User{}, params) + valid_invite? = invite && UserInviteToken.valid_invite?(invite) - with {:ok, user} <- User.register(changeset) do - !registrations_open && UserInviteToken.mark_as_used(token.token) + case invite do + nil -> + {:error, "Invalid token"} - {:ok, user} - else - {:error, changeset} -> - errors = - Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) - |> Jason.encode!() + invite when valid_invite? -> + UserInviteToken.update_usage!(invite) + create_user(params) - {:error, %{error: errors}} - end + _ -> + {:error, "Expired token"} + end + end - !registrations_open && is_nil(token) -> - {:error, "Invalid token"} + defp registration_process(true, params, _token) do + create_user(params) + end - !registrations_open && token.used -> - {:error, "Expired token"} - end + defp create_user(params) do + changeset = User.register_changeset(%User{}, params) + + case User.register(changeset) do + {:ok, user} -> + {:ok, user} + + {:error, changeset} -> + errors = + Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) + |> Jason.encode!() + + {:error, %{error: errors}} end end |