logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: e5d553aa45ffa218b0695d7976f012bfc1dcbafe
parent: 6504b43f96860d1911ed9a17d1c487a6e8ac93ba
Author: lambda <lain@soykaf.club>
Date:   Wed, 10 Apr 2019 10:10:08 +0000

Merge branch 'feature/767-multiple-use-invite-token' into 'develop'

Feature/767 multiple use invite token

See merge request pleroma/pleroma!1032

Diffstat:

Mdocs/api/admin_api.md59++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mlib/mix/tasks/pleroma/user.ex83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mlib/pleroma/user_invite_token.ex113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mlib/pleroma/web/admin_api/admin_api_controller.ex27+++++++++++++++++++++++----
Mlib/pleroma/web/admin_api/views/account_view.ex18++++++++++++++++++
Mlib/pleroma/web/router.ex2++
Mlib/pleroma/web/twitter_api/twitter_api.ex61+++++++++++++++++++++++++++++++++++++------------------------
Apriv/repo/migrations/20190404050946_add_fields_to_user_invite_tokens.exs12++++++++++++
Atest/fixtures/lambadalambda.json64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/support/http_request_mock.ex4++++
Mtest/tasks/user_test.exs82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Atest/user_invite_token_test.exs96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/web/admin_api/admin_api_controller_test.exs133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/web/twitter_api/twitter_api_test.exs347+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
14 files changed, 996 insertions(+), 105 deletions(-)

diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md @@ -200,12 +200,65 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ## `/api/pleroma/admin/invite_token` -### Get a account registeration invite token +### Get an account registration invite token - Methods: `GET` -- Params: none +- Params: + - *optional* `invite` => [ + - *optional* `max_use` (integer) + - *optional* `expires_at` (date string e.g. "2019-04-07") + ] - Response: invite token (base64 string) +## `/api/pleroma/admin/invites` + +### Get a list of generated invites + +- Methods: `GET` +- Params: none +- Response: + +```JSON +{ + + "invites": [ + { + "id": integer, + "token": string, + "used": boolean, + "expires_at": date, + "uses": integer, + "max_use": integer, + "invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`) + }, + ... + ] +} +``` + +## `/api/pleroma/admin/revoke_invite` + +### Revoke invite by token + +- Methods: `POST` +- Params: + - `token` +- Response: + +```JSON +{ + "id": integer, + "token": string, + "used": boolean, + "expires_at": date, + "uses": integer, + "max_use": integer, + "invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`) + +} +``` + + ## `/api/pleroma/admin/email_invite` ### Sends registration invite via email @@ -213,7 +266,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Methods: `POST` - Params: - `email` - - `name`, optionnal + - `name`, optional ## `/api/pleroma/admin/password_reset` diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 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 @@ -0,0 +1,12 @@ +defmodule Pleroma.Repo.Migrations.AddFieldsToUserInviteTokens do + use Ecto.Migration + + def change do + alter table(:user_invite_tokens) do + add(:expires_at, :date) + add(:uses, :integer, default: 0) + add(:max_use, :integer) + add(:invite_type, :string, default: "one_time") + end + end +end diff --git a/test/fixtures/lambadalambda.json b/test/fixtures/lambadalambda.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji", + "IdentityProof": "toot:IdentityProof", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://mastodon.social/users/lambadalambda", + "type": "Person", + "following": "https://mastodon.social/users/lambadalambda/following", + "followers": "https://mastodon.social/users/lambadalambda/followers", + "inbox": "https://mastodon.social/users/lambadalambda/inbox", + "outbox": "https://mastodon.social/users/lambadalambda/outbox", + "featured": "https://mastodon.social/users/lambadalambda/collections/featured", + "preferredUsername": "lambadalambda", + "name": "Critical Value", + "summary": "\u003cp\u003e\u003c/p\u003e", + "url": "https://mastodon.social/@lambadalambda", + "manuallyApprovesFollowers": false, + "publicKey": { + "id": "https://mastodon.social/users/lambadalambda#main-key", + "owner": "https://mastodon.social/users/lambadalambda", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw0P/Tq4gb4G/QVuMGbJo\nC/AfMNcv+m7NfrlOwkVzcU47jgESuYI4UtJayissCdBycHUnfVUd9qol+eznSODz\nCJhfJloqEIC+aSnuEPGA0POtWad6DU0E6/Ho5zQn5WAWUwbRQqowbrsm/GHo2+3v\neR5jGenwA6sYhINg/c3QQbksyV0uJ20Umyx88w8+TJuv53twOfmyDWuYNoQ3y5cc\nHKOZcLHxYOhvwg3PFaGfFHMFiNmF40dTXt9K96r7sbzc44iLD+VphbMPJEjkMuf8\nPGEFOBzy8pm3wJZw2v32RNW2VESwMYyqDzwHXGSq1a73cS7hEnc79gXlELsK04L9\nQQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [], + "endpoints": { + "sharedInbox": "https://mastodon.social/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif" + }, + "image": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://files.mastodon.social/accounts/headers/000/000/264/original/28b26104f83747d2.gif" + } +} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex @@ -716,6 +716,10 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.atom")}} end + def get("https://mastodon.social/users/lambadalambda", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/lambadalambda.json")}} + end + def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/activity+json") do {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)} end diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs @@ -245,7 +245,87 @@ 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 expires_at" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--expires-at", + 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 expires date" do + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invite", + "--max-use", + "5", + "--expires-at", + 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" do + test "invites are listed" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite() + + {:ok, invite2} = + Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 15}) + + # assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "invites" + ]) + + # 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.invite_type + assert message3 =~ invite2.invite_type + end + end + + describe "running revoke_invite" do + test "invite is revoked" do + {:ok, invite} = Pleroma.UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + assert capture_io(fn -> + Mix.Tasks.Pleroma.User.run([ + "revoke_invite", + invite.token + ]) + end) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Invite for token #{invite.token} was revoked." end end diff --git a/test/user_invite_token_test.exs b/test/user_invite_token_test.exs @@ -0,0 +1,96 @@ +defmodule Pleroma.UserInviteTokenTest do + use ExUnit.Case, async: true + use Pleroma.DataCase + alias Pleroma.UserInviteToken + + describe "valid_invite?/1 one time invites" do + setup do + invite = %UserInviteToken{invite_type: "one_time"} + + {:ok, invite: invite} + end + + test "not used returns true", %{invite: invite} do + invite = %{invite | used: false} + assert UserInviteToken.valid_invite?(invite) + end + + test "used returns false", %{invite: invite} do + invite = %{invite | used: true} + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_invite?/1 reusable invites" do + setup do + invite = %UserInviteToken{ + invite_type: "reusable", + max_use: 5 + } + + {:ok, invite: invite} + end + + test "with less uses then max use returns true", %{invite: invite} do + invite = %{invite | uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "with equal or more uses then max use returns false", %{invite: invite} do + invite = %{invite | uses: 5} + + refute UserInviteToken.valid_invite?(invite) + + invite = %{invite | uses: 6} + + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "date_limited"} + {:ok, invite: invite} + end + + test "expires today returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today()} + assert UserInviteToken.valid_invite?(invite) + end + + test "expires yesterday returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + invite = Repo.insert!(invite) + refute UserInviteToken.valid_invite?(invite) + end + end + + describe "valid_token?/1 reusable date limited invites" do + setup do + invite = %UserInviteToken{invite_type: "reusable_date_limited", max_use: 5} + {:ok, invite: invite} + end + + test "not overdue date and less uses returns true", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 4} + assert UserInviteToken.valid_invite?(invite) + end + + test "overdue date and less uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1)} + invite = Repo.insert!(invite) + refute UserInviteToken.valid_invite?(invite) + end + + test "not overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.utc_today(), uses: 5} + refute UserInviteToken.valid_invite?(invite) + end + + test "overdue date with more uses returns false", %{invite: invite} do + invite = %{invite | expires_at: Date.add(Date.utc_today(), -1), uses: 5} + invite = Repo.insert!(invite) + refute UserInviteToken.valid_invite?(invite) + end + end +end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do use Pleroma.Web.ConnCase alias Pleroma.User + alias Pleroma.UserInviteToken import Pleroma.Factory describe "/api/pleroma/admin/user" do @@ -640,4 +641,136 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "tags" => [] } end + + describe "GET /api/pleroma/admin/invite_token" do + test "without options" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token") + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + refute invite.used + refute invite.expires_at + refute invite.max_use + assert invite.invite_type == "one_time" + end + + test "with expires_at" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token", %{ + "invite" => %{"expires_at" => Date.to_string(Date.utc_today())} + }) + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + + refute invite.used + assert invite.expires_at == Date.utc_today() + refute invite.max_use + assert invite.invite_type == "date_limited" + end + + test "with max_use" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token", %{ + "invite" => %{"max_use" => 150} + }) + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + refute invite.used + refute invite.expires_at + assert invite.max_use == 150 + assert invite.invite_type == "reusable" + end + + test "with max use and expires_at" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invite_token", %{ + "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())} + }) + + token = json_response(conn, 200) + invite = UserInviteToken.find_by_token!(token) + refute invite.used + assert invite.expires_at == Date.utc_today() + assert invite.max_use == 150 + assert invite.invite_type == "reusable_date_limited" + end + end + + describe "GET /api/pleroma/admin/invites" do + test "no invites" do + admin = insert(:user, info: %{is_admin: true}) + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invites") + + assert json_response(conn, 200) == %{"invites" => []} + end + + test "with invite" do + admin = insert(:user, info: %{is_admin: true}) + {:ok, invite} = UserInviteToken.create_invite() + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/invites") + + assert json_response(conn, 200) == %{ + "invites" => [ + %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => false, + "uses" => 0 + } + ] + } + end + end + + describe "POST /api/pleroma/admin/revoke_invite" do + test "with token" do + admin = insert(:user, info: %{is_admin: true}) + {:ok, invite} = UserInviteToken.create_invite() + + conn = + build_conn() + |> assign(:user, admin) + |> post("/api/pleroma/admin/revoke_invite", %{"token" => invite.token}) + + assert json_response(conn, 200) == %{ + "expires_at" => nil, + "id" => invite.id, + "invite_type" => "one_time", + "max_use" => nil, + "token" => invite.token, + "used" => true, + "uses" => 0 + } + end + end end diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs @@ -16,6 +16,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do import Pleroma.Factory + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + test "create a status" do user = insert(:user) mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"}) @@ -299,7 +304,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do UserView.render("show.json", %{user: fetched_user}) end - @moduletag skip: "needs 'account_activation_required: true' in config" test "it sends confirmation email if :account_activation_required is specified in instance config" do setting = Pleroma.Config.get([:instance, :account_activation_required]) @@ -353,68 +357,313 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do assert user2.bio == expected_text end - @moduletag skip: "needs 'registrations_open: false' in config" - test "it registers a new user via invite token and returns the user." do - {:ok, token} = UserInviteToken.create_token() + describe "register with one time token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) - data = %{ - "nickname" => "vinny", - "email" => "pasta@pizza.vs", - "fullname" => "Vinny Vinesauce", - "bio" => "streamer", - "password" => "hiptofbees", - "confirm" => "hiptofbees", - "token" => token.token - } + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end - {:ok, user} = TwitterAPI.register_user(data) + :ok + end - fetched_user = User.get_by_nickname("vinny") - token = Repo.get_by(UserInviteToken, token: token.token) + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite() - assert token.used == true + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } - assert UserView.render("show.json", %{user: user}) == - UserView.render("show.json", %{user: fetched_user}) + {:ok, user} = TwitterAPI.register_user(data) + + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + assert invite.used == true + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + end + + test "returns error on invalid token" do + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => "DudeLetMeInImAFairy" + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Invalid token" + refute User.get_by_nickname("GrimReaper") + end + + test "returns error on expired token" do + {:ok, invite} = UserInviteToken.create_invite() + UserInviteToken.update_invite!(invite, used: true) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end end - @moduletag skip: "needs 'registrations_open: false' in config" - test "it returns an error if invalid token submitted" do - data = %{ - "nickname" => "GrimReaper", - "email" => "death@reapers.afterlife", - "fullname" => "Reaper Grim", - "bio" => "Your time has come", - "password" => "scythe", - "confirm" => "scythe", - "token" => "DudeLetMeInImAFairy" - } + describe "registers with date limited token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) + + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees" + } + + check_fn = fn invite -> + data = Map.put(data, "token", invite.token) + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + end + + {:ok, data: data, check_fn: check_fn} + end + + test "returns user on success", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()}) + + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end - {:error, msg} = TwitterAPI.register_user(data) + test "returns user on token which expired tomorrow", %{check_fn: check_fn} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)}) - assert msg == "Invalid token" - refute User.get_by_nickname("GrimReaper") + check_fn.(invite) + + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end + + test "returns an error on overdue date", %{data: data} do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)}) + + data = Map.put(data, "token", invite.token) + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + end end - @moduletag skip: "needs 'registrations_open: false' in config" - test "it returns an error if expired token submitted" do - {:ok, token} = UserInviteToken.create_token() - UserInviteToken.mark_as_used(token.token) + describe "registers with reusable token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) - data = %{ - "nickname" => "GrimReaper", - "email" => "death@reapers.afterlife", - "fullname" => "Reaper Grim", - "bio" => "Your time has come", - "password" => "scythe", - "confirm" => "scythe", - "token" => token.token - } + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end + + :ok + end + + test "returns user on success, after him registration fails" do + {:ok, invite} = UserInviteToken.create_invite(%{max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + assert invite.used == true + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end + end + + describe "registers with reusable date limited token" do + setup do + setting = Pleroma.Config.get([:instance, :registrations_open]) + + if setting do + Pleroma.Config.put([:instance, :registrations_open], false) + on_exit(fn -> Pleroma.Config.put([:instance, :registrations_open], setting) end) + end + + :ok + end + + test "returns user on success" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + + refute invite.used + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + end + + test "error after max uses" do + {:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 99) + + data = %{ + "nickname" => "vinny", + "email" => "pasta@pizza.vs", + "fullname" => "Vinny Vinesauce", + "bio" => "streamer", + "password" => "hiptofbees", + "confirm" => "hiptofbees", + "token" => invite.token + } + + {:ok, user} = TwitterAPI.register_user(data) + fetched_user = User.get_by_nickname("vinny") + invite = Repo.get_by(UserInviteToken, token: invite.token) + assert invite.used == true + + assert UserView.render("show.json", %{user: user}) == + UserView.render("show.json", %{user: fetched_user}) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end - {:error, msg} = TwitterAPI.register_user(data) + test "returns error on overdue date" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) - assert msg == "Expired token" - refute User.get_by_nickname("GrimReaper") + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end + + test "returns error on with overdue date and after max" do + {:ok, invite} = + UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100}) + + UserInviteToken.update_invite!(invite, uses: 100) + + data = %{ + "nickname" => "GrimReaper", + "email" => "death@reapers.afterlife", + "fullname" => "Reaper Grim", + "bio" => "Your time has come", + "password" => "scythe", + "confirm" => "scythe", + "token" => invite.token + } + + {:error, msg} = TwitterAPI.register_user(data) + + assert msg == "Expired token" + refute User.get_by_nickname("GrimReaper") + end end test "it returns the error on registration problems" do