Merge remote-tracking branch 'upstream/develop' into by-approval
authorAlex Gleason <alex@alexgleason.me>
Sun, 26 Jul 2020 20:46:14 +0000 (15:46 -0500)
committerAlex Gleason <alex@alexgleason.me>
Sun, 26 Jul 2020 20:46:14 +0000 (15:46 -0500)
24 files changed:
CHANGELOG.md
config/config.exs
config/description.exs
docs/API/admin_api.md
docs/configuration/cheatsheet.md
lib/pleroma/emails/admin_email.ex
lib/pleroma/moderation_log.ex
lib/pleroma/user.ex
lib/pleroma/user/query.ex
lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
lib/pleroma/web/admin_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/instance_view.ex
lib/pleroma/web/oauth/oauth_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api.ex
priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs [new file with mode: 0644]
test/emails/admin_email_test.exs
test/user_test.exs
test/web/admin_api/controllers/admin_api_controller_test.exs
test/web/admin_api/search_test.exs
test/web/mastodon_api/controllers/account_controller_test.exs
test/web/mastodon_api/controllers/instance_controller_test.exs
test/web/oauth/oauth_controller_test.exs
test/web/twitter_api/twitter_api_test.exs

index 9389df4bdfafea2cb78af0abc483d58aef859d12..c52d2eef5c67587d6f0f0beeada7697cf1e74c5d 100644 (file)
@@ -66,6 +66,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Support pagination in emoji packs API (for packs and for files in pack)
 - Support for viewing instances favicons next to posts and accounts
 - Added Pleroma.Upload.Filter.Exiftool as an alternate EXIF stripping mechanism targeting GPS/location metadata.
+- "By approval" registrations mode.
 
 <details>
   <summary>API Changes</summary>
index 406bf2a9bd367d1ba83f1bc3d60fd36968096d55..c5465ae2c062e84d5b1d1b9bad0f3d8e8f8169af 100644 (file)
@@ -205,6 +205,7 @@ config :pleroma, :instance,
   registrations_open: true,
   invites_enabled: false,
   account_activation_required: false,
+  account_approval_required: false,
   federating: true,
   federation_incoming_replies_max_depth: 100,
   federation_reachability_timeout_days: 7,
index e4850218e1b5ff11b1422f78f3d4e81fab6d71e3..f506affd4957c31d99691709d76d14d9f2238130 100644 (file)
@@ -661,6 +661,11 @@ config :pleroma, :config_description, [
         type: :boolean,
         description: "Require users to confirm their emails before signing in"
       },
+      %{
+        key: :account_approval_required,
+        type: :boolean,
+        description: "Require users to be manually approved by an admin before signing in"
+      },
       %{
         key: :federating,
         type: :boolean,
index baf895d90923fe7ed33f55e299caec0256f60d99..4b143e4eec0d7c7df57bf2d599a0f3b596fa65c6 100644 (file)
@@ -19,6 +19,7 @@ Configuration options:
     - `local`: only local users
     - `external`: only external users
     - `active`: only active users
+    - `need_approval`: only unapproved users
     - `deactivated`: only deactivated users
     - `is_admin`: users with admin role
     - `is_moderator`: users with moderator role
@@ -46,7 +47,10 @@ Configuration options:
       "local": bool,
       "tags": array,
       "avatar": string,
-      "display_name": string
+      "display_name": string,
+      "confirmation_pending": bool,
+      "approval_pending": bool,
+      "registration_reason": string,
     },
     ...
   ]
@@ -242,6 +246,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
 }
 ```
 
+## `PATCH /api/pleroma/admin/users/approve`
+
+### Approve user
+
+- Params:
+  - `nicknames`: nicknames array
+- Response:
+
+```json
+{
+  users: [
+    {
+      // user object
+    }
+  ]
+}
+```
+
 ## `GET /api/pleroma/admin/users/:nickname_or_id`
 
 ### Retrive the details of a user
index 042ad30c97caf0c223855e23029de3c1beebb45c..32563a2888c1414711508c018bd4a37b18691d34 100644 (file)
@@ -33,6 +33,7 @@ To add configuration to your config file, you can copy it from the base config.
 * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
 * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
 * `account_activation_required`: Require users to confirm their emails before signing in.
+* `account_approval_required`: Require users to be manually approved by an admin before signing in.
 * `federating`: Enable federation with other instances.
 * `federation_incoming_replies_max_depth`: Max. depth of reply-to activities fetching on incoming federation, to prevent out-of-memory situations while fetching very long threads. If set to `nil`, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.
 * `federation_reachability_timeout_days`: Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.
index aa0b2a66ba6724b95c8a304b4908e56d55b61966..fae7faf007cd9026a6c387ceab366d18205e4398 100644 (file)
@@ -82,4 +82,18 @@ defmodule Pleroma.Emails.AdminEmail do
     |> subject("#{instance_name()} Report")
     |> html_body(html_body)
   end
+
+  def new_unapproved_registration(to, account) do
+    html_body = """
+    <p>New account for review: <a href="#{user_url(account)}">@#{account.nickname}</a></p>
+    <blockquote>#{account.registration_reason}</blockquote>
+    <a href="#{Pleroma.Web.base_url()}/pleroma/admin">Visit AdminFE</a>
+    """
+
+    new()
+    |> to({to.name, to.email})
+    |> from({instance_name(), instance_notify_email()})
+    |> subject("New account up for review on #{instance_name()} (@#{account.nickname})")
+    |> html_body(html_body)
+  end
 end
index 7aacd9d80984edab08d4f38d408af1d5904e8846..31c9afe2a293a0f46ae944eee0479aa51c5aac57 100644 (file)
@@ -409,6 +409,17 @@ defmodule Pleroma.ModerationLog do
     "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
   end
 
+  @spec get_log_entry_message(ModerationLog) :: String.t()
+  def get_log_entry_message(%ModerationLog{
+        data: %{
+          "actor" => %{"nickname" => actor_nickname},
+          "action" => "approve",
+          "subject" => users
+        }
+      }) do
+    "@#{actor_nickname} approved users: #{users_to_nicknames_string(users)}"
+  end
+
   @spec get_log_entry_message(ModerationLog) :: String.t()
   def get_log_entry_message(%ModerationLog{
         data: %{
index 714ec9a4bac3dd74964f13a5212bfe246bcbe6e5..0138775bc989b8aaeb3b9842591b0454322619e9 100644 (file)
@@ -42,7 +42,12 @@ defmodule Pleroma.User do
   require Logger
 
   @type t :: %__MODULE__{}
-  @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
+  @type account_status ::
+          :active
+          | :deactivated
+          | :password_reset_pending
+          | :confirmation_pending
+          | :approval_pending
   @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
 
   # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@@ -106,6 +111,8 @@ defmodule Pleroma.User do
     field(:locked, :boolean, default: false)
     field(:confirmation_pending, :boolean, default: false)
     field(:password_reset_pending, :boolean, default: false)
+    field(:approval_pending, :boolean, default: false)
+    field(:registration_reason, :string, default: nil)
     field(:confirmation_token, :string, default: nil)
     field(:default_scope, :string, default: "public")
     field(:domain_blocks, {:array, :string}, default: [])
@@ -262,6 +269,7 @@ defmodule Pleroma.User do
   @spec account_status(User.t()) :: account_status()
   def account_status(%User{deactivated: true}), do: :deactivated
   def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
+  def account_status(%User{approval_pending: true}), do: :approval_pending
 
   def account_status(%User{confirmation_pending: true}) do
     if Config.get([:instance, :account_activation_required]) do
@@ -642,8 +650,16 @@ defmodule Pleroma.User do
         opts[:need_confirmation]
       end
 
+    need_approval? =
+      if is_nil(opts[:need_approval]) do
+        Config.get([:instance, :account_approval_required])
+      else
+        opts[:need_approval]
+      end
+
     struct
     |> confirmation_changeset(need_confirmation: need_confirmation?)
+    |> approval_changeset(need_approval: need_approval?)
     |> cast(params, [
       :bio,
       :raw_bio,
@@ -653,7 +669,8 @@ defmodule Pleroma.User do
       :password,
       :password_confirmation,
       :emoji,
-      :accepts_chat_messages
+      :accepts_chat_messages,
+      :registration_reason
     ])
     |> validate_required([:name, :nickname, :password, :password_confirmation])
     |> validate_confirmation(:password)
@@ -1473,6 +1490,19 @@ defmodule Pleroma.User do
     end
   end
 
+  def approve(users) when is_list(users) do
+    Repo.transaction(fn ->
+      Enum.map(users, fn user ->
+        with {:ok, user} <- approve(user), do: user
+      end)
+    end)
+  end
+
+  def approve(%User{} = user) do
+    change(user, approval_pending: false)
+    |> update_and_set_cache()
+  end
+
   def update_notification_settings(%User{} = user, settings) do
     user
     |> cast(%{notification_settings: settings}, [])
@@ -1499,12 +1529,13 @@ defmodule Pleroma.User do
   defp delete_or_deactivate(%User{local: true} = user) do
     status = account_status(user)
 
-    if status == :confirmation_pending do
-      delete_and_invalidate_cache(user)
-    else
-      user
-      |> change(%{deactivated: true, email: nil})
-      |> update_and_set_cache()
+    case status do
+      :confirmation_pending -> delete_and_invalidate_cache(user)
+      :approval_pending -> delete_and_invalidate_cache(user)
+      _ ->
+        user
+        |> change(%{deactivated: true, email: nil})
+        |> update_and_set_cache()
     end
   end
 
@@ -2157,6 +2188,12 @@ defmodule Pleroma.User do
     cast(user, params, [:confirmation_pending, :confirmation_token])
   end
 
+  @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
+  def approval_changeset(user, need_approval: need_approval?) do
+    params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
+    cast(user, params, [:approval_pending])
+  end
+
   def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
     if id not in user.pinned_activities do
       max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
index 66ffe909031637f284f66c2450a8701ffc4e8add..45553cb6c9a6b407f4aace4ce7c917f508cf6aec 100644 (file)
@@ -42,6 +42,7 @@ defmodule Pleroma.User.Query do
             external: boolean(),
             active: boolean(),
             deactivated: boolean(),
+            need_approval: boolean(),
             is_admin: boolean(),
             is_moderator: boolean(),
             super_users: boolean(),
@@ -146,6 +147,10 @@ defmodule Pleroma.User.Query do
     |> where([u], not is_nil(u.nickname))
   end
 
+  defp compose_query({:need_approval, _}, query) do
+    where(query, [u], u.approval_pending)
+  end
+
   defp compose_query({:followers, %User{id: id}}, query) do
     query
     |> where([u], u.id != ^id)
index 5101e28d62ed83ba83e5daf8b14f243e3f2763a0..aa2af1ab5495a4715f79d11c0a8a2bffc1ae01e5 100644 (file)
@@ -44,6 +44,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
            :user_toggle_activation,
            :user_activate,
            :user_deactivate,
+           :user_approve,
            :tag_users,
            :untag_users,
            :right_add,
@@ -303,6 +304,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     |> render("index.json", %{users: Keyword.values(updated_users)})
   end
 
+  def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
+    {:ok, updated_users} = User.approve(users)
+
+    ModerationLog.insert_log(%{
+      actor: admin,
+      subject: users,
+      action: "approve"
+    })
+
+    conn
+    |> put_view(AccountView)
+    |> render("index.json", %{users: updated_users})
+  end
+
   def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
     with {:ok, _} <- User.tag(nicknames, tags) do
       ModerationLog.insert_log(%{
@@ -354,7 +370,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     end
   end
 
-  @filters ~w(local external active deactivated is_admin is_moderator)
+  @filters ~w(local external active deactivated need_approval is_admin is_moderator)
 
   @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
   defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
index 88fbb53159a24abafc911fdd17665f97f808bd17..333e72e42ab083743378d14b969dfa5fee810183 100644 (file)
@@ -77,7 +77,9 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
       "roles" => User.roles(user),
       "tags" => user.tags || [],
       "confirmation_pending" => user.confirmation_pending,
-      "url" => user.uri || user.ap_id
+      "approval_pending" => user.approval_pending,
+      "url" => user.uri || user.ap_id,
+      "registration_reason" => user.registration_reason
     }
   end
 
index cd3bc7f00c525f5413a6be3e38613dc44e3b9167..ea2d3aa9c9603cb26ee30b4bb7c9bb6c42fd8a3c 100644 (file)
@@ -26,6 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
       thumbnail: Keyword.get(instance, :instance_thumbnail),
       languages: ["en"],
       registrations: Keyword.get(instance, :registrations_open),
+      approval_required: Keyword.get(instance, :account_approval_required),
       # Extra (not present in Mastodon):
       max_toot_chars: Keyword.get(instance, :limit),
       poll_limits: Keyword.get(instance, :poll_limits),
index 7683589cf2b347fb6d604c1e7a5bca788710041f..61fe81d331fc0bc0e40e0a099aa86ee533df242d 100644 (file)
@@ -337,6 +337,16 @@ defmodule Pleroma.Web.OAuth.OAuthController do
     )
   end
 
+  defp handle_token_exchange_error(%Plug.Conn{} = conn, {:account_status, :approval_pending}) do
+    render_error(
+      conn,
+      :forbidden,
+      "Your account is awaiting approval.",
+      %{},
+      "awaiting_approval"
+    )
+  end
+
   defp handle_token_exchange_error(%Plug.Conn{} = conn, _error) do
     render_invalid_credentials_error(conn)
   end
index 386308362b6baaf295b70932e3270846cfce2b2f..c6433cc5325f3e923dc9150632c82ec5945d7d5a 100644 (file)
@@ -138,6 +138,7 @@ defmodule Pleroma.Web.Router do
     patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
     patch("/users/activate", AdminAPIController, :user_activate)
     patch("/users/deactivate", AdminAPIController, :user_deactivate)
+    patch("/users/approve", AdminAPIController, :user_approve)
     put("/users/tag", AdminAPIController, :tag_users)
     delete("/users/tag", AdminAPIController, :untag_users)
 
index 5cfb385ac3068e84e36de42f7026abfeff5005a9..424a705ddaa679196a169948705409e1b8ad48d6 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
 
   alias Pleroma.Emails.Mailer
   alias Pleroma.Emails.UserEmail
+  alias Pleroma.HTML
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.UserInviteToken
@@ -19,6 +20,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
       |> Map.put(:nickname, params[:username])
       |> Map.put(:name, Map.get(params, :fullname, params[:username]))
       |> Map.put(:password_confirmation, params[:password])
+      |> Map.put(:registration_reason, HTML.strip_tags(params[:reason]))
 
     if Pleroma.Config.get([:instance, :registrations_open]) do
       create_user(params, opts)
@@ -44,6 +46,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
 
     case User.register(changeset) do
       {:ok, user} ->
+        maybe_notify_admins(user)
         {:ok, user}
 
       {:error, changeset} ->
@@ -56,6 +59,18 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     end
   end
 
+  defp maybe_notify_admins(%User{} = account) do
+    if Pleroma.Config.get([:instance, :account_approval_required]) do
+      User.all_superusers()
+      |> Enum.filter(fn user -> not is_nil(user.email) end)
+      |> Enum.each(fn superuser ->
+        superuser
+        |> Pleroma.Emails.AdminEmail.new_unapproved_registration(account)
+        |> Pleroma.Emails.Mailer.deliver_async()
+      end)
+    end
+  end
+
   def password_reset(nickname_or_email) do
     with true <- is_binary(nickname_or_email),
          %User{local: true, email: email} = user when is_binary(email) <-
diff --git a/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs b/priv/repo/migrations/20200712234852_add_approval_fields_to_users.exs
new file mode 100644 (file)
index 0000000..559640f
--- /dev/null
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.AddApprovalFieldsToUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      add(:approval_pending, :boolean)
+      add(:registration_reason, :string)
+    end
+  end
+end
index 9082ae5a7c2a7eaabdff516b2ab7f74d43e013a2..e24231e270553340075277cca73edb8cf4e5c2bd 100644 (file)
@@ -46,4 +46,24 @@ defmodule Pleroma.Emails.AdminEmailTest do
     assert res.to == [{to_user.name, to_user.email}]
     assert res.from == {config[:name], config[:notify_email]}
   end
+
+  test "new unapproved registration email" do
+    config = Pleroma.Config.get(:instance)
+    to_user = insert(:user)
+    account = insert(:user, registration_reason: "Plz let me in")
+
+    res = AdminEmail.new_unapproved_registration(to_user, account)
+
+    account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id)
+
+    assert res.to == [{to_user.name, to_user.email}]
+    assert res.from == {config[:name], config[:notify_email]}
+    assert res.subject == "New account up for review on #{config[:name]} (@#{account.nickname})"
+
+    assert res.html_body == """
+           <p>New account for review: <a href="#{account_url}">@#{account.nickname}</a></p>
+           <blockquote>Plz let me in</blockquote>
+           <a href="http://localhost:4001/pleroma/admin">Visit AdminFE</a>
+           """
+  end
 end
index 21c03b470fab5ff4c1e098e10610ff5ecf1f54bc..8cdf3a89118448760ae811ebedab6b581fddf559 100644 (file)
@@ -526,6 +526,27 @@ defmodule Pleroma.UserTest do
     end
   end
 
+  describe "user registration, with :account_approval_required" do
+    @full_user_data %{
+      bio: "A guy",
+      name: "my name",
+      nickname: "nick",
+      password: "test",
+      password_confirmation: "test",
+      email: "email@example.com"
+    }
+    setup do: clear_config([:instance, :account_approval_required], true)
+
+    test "it creates unapproved user" do
+      changeset = User.register_changeset(%User{}, @full_user_data)
+      assert changeset.valid?
+
+      {:ok, user} = Repo.insert(changeset)
+
+      assert user.approval_pending
+    end
+  end
+
   describe "get_or_fetch/1" do
     test "gets an existing user by nickname" do
       user = insert(:user)
@@ -1191,6 +1212,31 @@ defmodule Pleroma.UserTest do
     end
   end
 
+  describe "approve" do
+    test "approves a user" do
+      user = insert(:user, approval_pending: true)
+      assert true == user.approval_pending
+      {:ok, user} = User.approve(user)
+      assert false == user.approval_pending
+    end
+
+    test "approves a list of users" do
+      unapproved_users = [
+        insert(:user, approval_pending: true),
+        insert(:user, approval_pending: true),
+        insert(:user, approval_pending: true)
+      ]
+
+      {:ok, users} = User.approve(unapproved_users)
+
+      assert Enum.count(users) == 3
+
+      Enum.each(users, fn user ->
+        assert false == user.approval_pending
+      end)
+    end
+  end
+
   describe "delete" do
     setup do
       {:ok, user} = insert(:user) |> User.set_cache()
@@ -1278,6 +1324,17 @@ defmodule Pleroma.UserTest do
     end
   end
 
+  test "delete/1 when approval is pending deletes the user" do
+    user = insert(:user, approval_pending: true)
+    {:ok, user: user}
+
+    {:ok, job} = User.delete(user)
+    {:ok, _} = ObanHelpers.perform(job)
+
+    refute User.get_cached_by_id(user.id)
+    refute User.get_by_id(user.id)
+  end
+
   test "get_public_key_for_ap_id fetches a user that's not in the db" do
     assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
   end
@@ -1352,6 +1409,14 @@ defmodule Pleroma.UserTest do
       user = insert(:user, local: true, confirmation_pending: false, deactivated: true)
       assert User.account_status(user) == :deactivated
     end
+
+    test "returns :approval_pending for unapproved user" do
+      user = insert(:user, local: true, approval_pending: true)
+      assert User.account_status(user) == :approval_pending
+
+      user = insert(:user, local: true, confirmation_pending: true, approval_pending: true)
+      assert User.account_status(user) == :approval_pending
+    end
   end
 
   describe "superuser?/1" do
index 6082441ee5e8e032494da8b8810d08ca2be22e0e..b5d5bd8c70e837bc6ca09825174a7c24cdf0ba5e 100644 (file)
@@ -349,7 +349,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
         "avatar" => User.avatar_url(user) |> MediaProxy.url(),
         "display_name" => HTML.strip_tags(user.name || user.nickname),
         "confirmation_pending" => false,
-        "url" => user.ap_id
+        "approval_pending" => false,
+        "url" => user.ap_id,
+        "registration_reason" => nil
       }
 
       assert expected == json_response(conn, 200)
@@ -613,6 +615,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
   describe "GET /api/pleroma/admin/users" do
     test "renders users array for the first page", %{conn: conn, admin: admin} do
       user = insert(:user, local: false, tags: ["foo", "bar"])
+      user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude")
+
       conn = get(conn, "/api/pleroma/admin/users?page=1")
 
       users =
@@ -627,7 +631,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
-            "url" => admin.ap_id
+            "approval_pending" => false,
+            "url" => admin.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => user.deactivated,
@@ -639,13 +645,29 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
             "confirmation_pending" => false,
-            "url" => user.ap_id
+            "approval_pending" => false,
+            "url" => user.ap_id,
+            "registration_reason" => nil
+          },
+          %{
+            "deactivated" => user2.deactivated,
+            "id" => user2.id,
+            "nickname" => user2.nickname,
+            "roles" => %{"admin" => false, "moderator" => false},
+            "local" => true,
+            "tags" => [],
+            "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
+            "display_name" => HTML.strip_tags(user2.name || user2.nickname),
+            "confirmation_pending" => false,
+            "approval_pending" => true,
+            "url" => user2.ap_id,
+            "registration_reason" => "I'm a chill dude"
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
 
       assert json_response(conn, 200) == %{
-               "count" => 2,
+               "count" => 3,
                "page_size" => 50,
                "users" => users
              }
@@ -712,7 +734,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -738,7 +762,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -764,7 +790,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -790,7 +818,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -816,7 +846,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -842,7 +874,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -863,7 +897,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user2.name || user2.nickname),
                    "confirmation_pending" => false,
-                   "url" => user2.ap_id
+                   "approval_pending" => false,
+                   "url" => user2.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -896,7 +932,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -922,7 +960,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(user) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user.name || user.nickname),
             "confirmation_pending" => false,
-            "url" => user.ap_id
+            "approval_pending" => false,
+            "url" => user.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => admin.deactivated,
@@ -934,7 +974,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
-            "url" => admin.ap_id
+            "approval_pending" => false,
+            "url" => admin.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => false,
@@ -946,7 +988,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
             "confirmation_pending" => false,
-            "url" => old_admin.ap_id
+            "approval_pending" => false,
+            "url" => old_admin.ap_id,
+            "registration_reason" => nil
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -958,6 +1002,44 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
              }
     end
 
+    test "only unapproved users", %{conn: conn} do
+      user =
+        insert(:user,
+          nickname: "sadboy",
+          approval_pending: true,
+          registration_reason: "Plz let me in!"
+        )
+
+      insert(:user, nickname: "happyboy", approval_pending: false)
+
+      conn = get(conn, "/api/pleroma/admin/users?filters=need_approval")
+
+      users =
+        [
+          %{
+            "deactivated" => user.deactivated,
+            "id" => user.id,
+            "nickname" => user.nickname,
+            "roles" => %{"admin" => false, "moderator" => false},
+            "local" => true,
+            "tags" => [],
+            "avatar" => User.avatar_url(user) |> MediaProxy.url(),
+            "display_name" => HTML.strip_tags(user.name || user.nickname),
+            "confirmation_pending" => false,
+            "approval_pending" => true,
+            "url" => user.ap_id,
+            "registration_reason" => "Plz let me in!"
+          }
+        ]
+        |> Enum.sort_by(& &1["nickname"])
+
+      assert json_response(conn, 200) == %{
+               "count" => 1,
+               "page_size" => 50,
+               "users" => users
+             }
+    end
+
     test "load only admins", %{conn: conn, admin: admin} do
       second_admin = insert(:user, is_admin: true)
       insert(:user)
@@ -977,7 +1059,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(admin.name || admin.nickname),
             "confirmation_pending" => false,
-            "url" => admin.ap_id
+            "approval_pending" => false,
+            "url" => admin.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => false,
@@ -989,7 +1073,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
             "confirmation_pending" => false,
-            "url" => second_admin.ap_id
+            "approval_pending" => false,
+            "url" => second_admin.ap_id,
+            "registration_reason" => nil
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -1022,7 +1108,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
                    "confirmation_pending" => false,
-                   "url" => moderator.ap_id
+                   "approval_pending" => false,
+                   "url" => moderator.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -1048,7 +1136,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user1.name || user1.nickname),
             "confirmation_pending" => false,
-            "url" => user1.ap_id
+            "approval_pending" => false,
+            "url" => user1.ap_id,
+            "registration_reason" => nil
           },
           %{
             "deactivated" => false,
@@ -1060,7 +1150,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
             "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
             "display_name" => HTML.strip_tags(user2.name || user2.nickname),
             "confirmation_pending" => false,
-            "url" => user2.ap_id
+            "approval_pending" => false,
+            "url" => user2.ap_id,
+            "registration_reason" => nil
           }
         ]
         |> Enum.sort_by(& &1["nickname"])
@@ -1100,7 +1192,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(user.name || user.nickname),
                    "confirmation_pending" => false,
-                   "url" => user.ap_id
+                   "approval_pending" => false,
+                   "url" => user.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -1125,7 +1219,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                    "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
                    "display_name" => HTML.strip_tags(admin.name || admin.nickname),
                    "confirmation_pending" => false,
-                   "url" => admin.ap_id
+                   "approval_pending" => false,
+                   "url" => admin.ap_id,
+                   "registration_reason" => nil
                  }
                ]
              }
@@ -1172,6 +1268,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
              "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
   end
 
+  test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do
+    user_one = insert(:user, approval_pending: true)
+    user_two = insert(:user, approval_pending: true)
+
+    conn =
+      patch(
+        conn,
+        "/api/pleroma/admin/users/approve",
+        %{nicknames: [user_one.nickname, user_two.nickname]}
+      )
+
+    response = json_response(conn, 200)
+    assert Enum.map(response["users"], & &1["approval_pending"]) == [false, false]
+
+    log_entry = Repo.one(ModerationLog)
+
+    assert ModerationLog.get_log_entry_message(log_entry) ==
+             "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}"
+  end
+
   test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
     user = insert(:user)
 
@@ -1188,7 +1304,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
                "avatar" => User.avatar_url(user) |> MediaProxy.url(),
                "display_name" => HTML.strip_tags(user.name || user.nickname),
                "confirmation_pending" => false,
-               "url" => user.ap_id
+               "approval_pending" => false,
+               "url" => user.ap_id,
+               "registration_reason" => nil
              }
 
     log_entry = Repo.one(ModerationLog)
index e0e3d4153895d04966665674916682b4de522154..b974cedd5a2dd7ce11566bef41b76a0534c2036e 100644 (file)
@@ -166,5 +166,16 @@ defmodule Pleroma.Web.AdminAPI.SearchTest do
       assert total == 3
       assert count == 1
     end
+
+    test "it returns unapproved user" do
+      unapproved = insert(:user, approval_pending: true)
+      insert(:user)
+      insert(:user)
+
+      {:ok, _results, total} = Search.user()
+      {:ok, [^unapproved], count} = Search.user(%{need_approval: true})
+      assert total == 3
+      assert count == 1
+    end
   end
 end
index c304487eae581373957b7c04ee475ee627c0ee7d..e6b283aab390790074cb911678363b60f21038fa 100644 (file)
@@ -904,6 +904,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
     end
 
     setup do: clear_config([:instance, :account_activation_required])
+    setup do: clear_config([:instance, :account_approval_required])
 
     test "Account registration via Application", %{conn: conn} do
       conn =
@@ -968,6 +969,75 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       assert token_from_db.user.confirmation_pending
     end
 
+    test "Account registration via app with account_approval_required", %{conn: conn} do
+      Pleroma.Config.put([:instance, :account_approval_required], true)
+
+      conn =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/apps", %{
+          client_name: "client_name",
+          redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
+          scopes: "read, write, follow"
+        })
+
+      assert %{
+               "client_id" => client_id,
+               "client_secret" => client_secret,
+               "id" => _,
+               "name" => "client_name",
+               "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
+               "vapid_key" => _,
+               "website" => nil
+             } = json_response_and_validate_schema(conn, 200)
+
+      conn =
+        post(conn, "/oauth/token", %{
+          grant_type: "client_credentials",
+          client_id: client_id,
+          client_secret: client_secret
+        })
+
+      assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
+               json_response(conn, 200)
+
+      assert token
+      token_from_db = Repo.get_by(Token, token: token)
+      assert token_from_db
+      assert refresh
+      assert scope == "read write follow"
+
+      conn =
+        build_conn()
+        |> put_req_header("content-type", "multipart/form-data")
+        |> put_req_header("authorization", "Bearer " <> token)
+        |> post("/api/v1/accounts", %{
+          username: "lain",
+          email: "lain@example.org",
+          password: "PlzDontHackLain",
+          bio: "Test Bio",
+          agreement: true,
+          reason: "I'm a cool dude, bro"
+        })
+
+      %{
+        "access_token" => token,
+        "created_at" => _created_at,
+        "scope" => ^scope,
+        "token_type" => "Bearer"
+      } = json_response_and_validate_schema(conn, 200)
+
+      token_from_db = Repo.get_by(Token, token: token)
+      assert token_from_db
+      token_from_db = Repo.preload(token_from_db, :user)
+      assert token_from_db.user
+
+      assert token_from_db.user.confirmation_pending
+      assert token_from_db.user.approval_pending
+
+      assert token_from_db.user.registration_reason == "I'm a cool dude, bro"
+    end
+
     test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do
       _user = insert(:user, email: "lain@example.org")
       app_token = insert(:oauth_token, user: nil)
index cc880d82ca4ab814e73cb04debee6d128bf04b65..6a9ccd97983c6f9dc50b2026b0a90045d0cb5007 100644 (file)
@@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
              "thumbnail" => _,
              "languages" => _,
              "registrations" => _,
+             "approval_required" => _,
              "poll_limits" => _,
              "upload_limit" => _,
              "avatar_upload_limit" => _,
index d389e4ce053ceee468de3ec6c8e1a747f681bba5..1200126b81dca7b5b45be13b95447dc8eed6d3a2 100644 (file)
@@ -19,7 +19,10 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
     key: "_test",
     signing_salt: "cooldude"
   ]
-  setup do: clear_config([:instance, :account_activation_required])
+  setup do
+    clear_config([:instance, :account_activation_required])
+    clear_config([:instance, :account_approval_required])
+  end
 
   describe "in OAuth consumer mode, " do
     setup do
@@ -995,6 +998,30 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
              }
     end
 
+    test "rejects token exchange for valid credentials belonging to an unapproved user" do
+      password = "testpassword"
+
+      user = insert(:user, password_hash: Pbkdf2.hash_pwd_salt(password), approval_pending: true)
+
+      refute Pleroma.User.account_status(user) == :active
+
+      app = insert(:oauth_app)
+
+      conn =
+        build_conn()
+        |> post("/oauth/token", %{
+          "grant_type" => "password",
+          "username" => user.nickname,
+          "password" => password,
+          "client_id" => app.client_id,
+          "client_secret" => app.client_secret
+        })
+
+      assert resp = json_response(conn, 403)
+      assert %{"error" => _} = resp
+      refute Map.has_key?(resp, "access_token")
+    end
+
     test "rejects an invalid authorization code" do
       app = insert(:oauth_app)
 
index 5bb2d8d89e1902eeb7f5b1412f7898c0e4876964..20a45cb6f94d74caa2f65c6333844576e512a7ff 100644 (file)
@@ -4,7 +4,7 @@
 
 defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
   use Pleroma.DataCase
-
+  import Pleroma.Factory
   alias Pleroma.Repo
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
@@ -79,6 +79,42 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
     )
   end
 
+  test "it sends an admin email if :account_approval_required is specified in instance config" do
+    admin = insert(:user, is_admin: true)
+    setting = Pleroma.Config.get([:instance, :account_approval_required])
+
+    unless setting do
+      Pleroma.Config.put([:instance, :account_approval_required], true)
+      on_exit(fn -> Pleroma.Config.put([:instance, :account_approval_required], setting) end)
+    end
+
+    data = %{
+      :username => "lain",
+      :email => "lain@wired.jp",
+      :fullname => "lain iwakura",
+      :bio => "",
+      :password => "bear",
+      :confirm => "bear",
+      :reason => "I love anime"
+    }
+
+    {:ok, user} = TwitterAPI.register_user(data)
+    ObanHelpers.perform_all()
+
+    assert user.approval_pending
+
+    email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user)
+
+    notify_email = Pleroma.Config.get([:instance, :notify_email])
+    instance_name = Pleroma.Config.get([:instance, :name])
+
+    Swoosh.TestAssertions.assert_email_sent(
+      from: {instance_name, notify_email},
+      to: {admin.name, admin.email},
+      html_body: email.html_body
+    )
+  end
+
   test "it registers a new user and parses mentions in the bio" do
     data1 = %{
       :username => "john",