Merge branch 'feature/767-multiple-use-invite-token' into 'develop'
Feature/767 multiple use invite token See merge request pleroma/pleroma!1032
This commit is contained in:
commit
e5d553aa45
14 changed files with 1003 additions and 112 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
token = %UserInviteToken{
|
||||
used: false,
|
||||
token: token
|
||||
}
|
||||
|
||||
Repo.insert(token)
|
||||
put_change(changeset, :token, token)
|
||||
end
|
||||
|
||||
def used_changeset(struct) do
|
||||
struct
|
||||
|> cast(%{}, [])
|
||||
|> put_change(:used, true)
|
||||
defp assign_type(%{changes: %{max_use: _max_use, expires_at: _expires_at}} = changeset) do
|
||||
put_change(changeset, :invite_type, "reusable_date_limited")
|
||||
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}
|
||||
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")
|
||||
|
||||
@spec list_invites() :: [UserInviteToken.t()]
|
||||
def list_invites do
|
||||
query = from(u in UserInviteToken, order_by: u.id)
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
@spec update_invite!(UserInviteToken.t(), map()) :: UserInviteToken.t() | no_return()
|
||||
def update_invite!(invite, changes) do
|
||||
change(invite, changes) |> Repo.update!()
|
||||
end
|
||||
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
cond do
|
||||
registrations_open || (!is_nil(token) && !token.used) ->
|
||||
changeset = User.register_changeset(%User{}, params)
|
||||
|
||||
with {:ok, user} <- User.register(changeset) do
|
||||
!registrations_open && UserInviteToken.mark_as_used(token.token)
|
||||
|
||||
{:ok, user}
|
||||
else
|
||||
{:error, changeset} ->
|
||||
errors =
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
||||
|> Jason.encode!()
|
||||
|
||||
{:error, %{error: errors}}
|
||||
end
|
||||
|
||||
!registrations_open && is_nil(token) ->
|
||||
{:error, "Invalid token"}
|
||||
|
||||
!registrations_open && token.used ->
|
||||
{:error, "Expired token"}
|
||||
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
|
||||
|
||||
valid_invite? = invite && UserInviteToken.valid_invite?(invite)
|
||||
|
||||
case invite do
|
||||
nil ->
|
||||
{:error, "Invalid token"}
|
||||
|
||||
invite when valid_invite? ->
|
||||
UserInviteToken.update_usage!(invite)
|
||||
create_user(params)
|
||||
|
||||
_ ->
|
||||
{:error, "Expired token"}
|
||||
end
|
||||
end
|
||||
|
||||
defp registration_process(true, params, _token) do
|
||||
create_user(params)
|
||||
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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue