From 46eb160135cf408fee87bb76fc46acfc51af901d Mon Sep 17 00:00:00 2001
From: Maxim Filippov <colixer@gmail.com>
Date: Tue, 19 Nov 2019 20:14:02 +0900
Subject: [PATCH] AdminAPI: Confirm user account, resend confirmation email

---
 CHANGELOG.md                                  |  1 +
 docs/API/admin_api.md                         | 16 +++++
 lib/pleroma/moderation_log.ex                 | 26 +++++++-
 lib/pleroma/user.ex                           |  9 +++
 .../web/admin_api/admin_api_controller.ex     | 41 ++++++++++--
 .../web/admin_api/views/account_view.ex       |  3 +-
 lib/pleroma/web/router.ex                     |  3 +
 .../admin_api/admin_api_controller_test.exs   | 62 +++++++++++++++++++
 8 files changed, 153 insertions(+), 8 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4ad91b0d..4b33718a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -67,6 +67,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
 - Configuration: `feed` option for user atom feed.
 - Pleroma API: Add Emoji reactions
+- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
 </details>
 
 ### Fixed
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index 9d914c9a6..d943b3ee3 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -878,3 +878,19 @@ Compile time settings (need instance reboot):
 - Authentication: required
 - Params: None
 - Response: JSON, "ok" and 200 status
+
+## `PATCH /api/pleroma/admin/users/confirm_email`
+
+### Confirm users' emails
+
+- Params:
+  - `nicknames`
+- Response: Array of user nicknames
+
+## `PATCH /api/pleroma/admin/users/resend_confirmation_email`
+
+### Resend confirmation email
+
+- Params:
+  - `nicknames`
+- Response: Array of user nicknames
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
index ffa5dc25d..706f089dc 100644
--- a/lib/pleroma/moderation_log.ex
+++ b/lib/pleroma/moderation_log.ex
@@ -624,7 +624,31 @@ def get_log_entry_message(%ModerationLog{
           "subject" => subjects
         }
       }) do
-    "@#{actor_nickname} force password reset for users: #{users_to_nicknames_string(subjects)}"
+    "@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
+  end
+
+  @spec get_log_entry_message(ModerationLog) :: String.t()
+  def get_log_entry_message(%ModerationLog{
+        data: %{
+          "actor" => %{"nickname" => actor_nickname},
+          "action" => "confirm_email",
+          "subject" => subjects
+        }
+      }) do
+    "@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
+  end
+
+  @spec get_log_entry_message(ModerationLog) :: String.t()
+  def get_log_entry_message(%ModerationLog{
+        data: %{
+          "actor" => %{"nickname" => actor_nickname},
+          "action" => "resend_confirmation_email",
+          "subject" => subjects
+        }
+      }) do
+    "@#{actor_nickname} re-sent confirmation email for users: #{
+      users_to_nicknames_string(subjects)
+    }"
   end
 
   defp nicknames_to_string(nicknames) do
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index f8c2db1e1..f031d2179 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -489,6 +489,10 @@ def try_send_confirmation_email(%User{} = user) do
     end
   end
 
+  def try_send_confirmation_email(users) do
+    Enum.map(users, &try_send_confirmation_email/1)
+  end
+
   def needs_update?(%User{local: true}), do: false
 
   def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@@ -1572,6 +1576,11 @@ def toggle_confirmation(%User{} = user) do
     |> update_and_set_cache()
   end
 
+  @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
+  def toggle_confirmation(users) do
+    Enum.map(users, &toggle_confirmation/1)
+  end
+
   def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
     mascot
   end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 8c1318d1b..c46ce76da 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -335,7 +335,7 @@ def list_users(conn, params) do
     }
 
     with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
-         {:ok, users, count} <- filter_relay_user(users, count),
+         {:ok, users, count} <- filter_service_users(users, count),
          do:
            conn
            |> json(
@@ -347,15 +347,16 @@ def list_users(conn, params) do
            )
   end
 
-  defp filter_relay_user(users, count) do
-    filtered_users = Enum.reject(users, &relay_user?/1)
-    count = if Enum.any?(users, &relay_user?/1), do: length(filtered_users), else: count
+  defp filter_service_users(users, count) do
+    filtered_users = Enum.reject(users, &service_user?/1)
+    count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
 
     {:ok, filtered_users, count}
   end
 
-  defp relay_user?(user) do
-    user.ap_id == Relay.relay_ap_id()
+  defp service_user?(user) do
+    String.match?(user.ap_id, ~r/.*\/relay$/) or
+      String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
   end
 
   @filters ~w(local external active deactivated is_admin is_moderator)
@@ -799,6 +800,34 @@ def reload_emoji(conn, _params) do
     conn |> json("ok")
   end
 
+  def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+
+    User.toggle_confirmation(users)
+
+    ModerationLog.insert_log(%{
+      actor: admin,
+      subject: users,
+      action: "confirm_email"
+    })
+
+    conn |> json("")
+  end
+
+  def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
+    users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
+
+    User.try_send_confirmation_email(users)
+
+    ModerationLog.insert_log(%{
+      actor: admin,
+      subject: users,
+      action: "resend_confirmation_email"
+    })
+
+    conn |> json("")
+  end
+
   def errors(conn, {:error, :not_found}) do
     conn
     |> put_status(:not_found)
diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex
index 6aa7257ce..d9dba5c51 100644
--- a/lib/pleroma/web/admin_api/views/account_view.ex
+++ b/lib/pleroma/web/admin_api/views/account_view.ex
@@ -36,7 +36,8 @@ def render("show.json", %{user: user}) do
       "deactivated" => user.deactivated,
       "local" => user.local,
       "roles" => User.roles(user),
-      "tags" => user.tags || []
+      "tags" => user.tags || [],
+      "confirmation_pending" => user.confirmation_pending
     }
   end
 
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 9b8b373b8..b654d00c7 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -177,6 +177,9 @@ defmodule Pleroma.Web.Router do
     get("/users/:nickname", AdminAPIController, :user_show)
     get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
 
+    patch("/users/confirm_email", AdminAPIController, :confirm_email)
+    patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
+
     get("/reports", AdminAPIController, :list_reports)
     get("/grouped_reports", AdminAPIController, :list_grouped_reports)
     get("/reports/:id", AdminAPIController, :report_show)
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 3a4c4d65c..8fabfd963 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -2839,6 +2839,68 @@ test "DELETE /relay", %{admin: admin} do
                "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
     end
   end
+
+  describe "PATCH /confirm_email" do
+    setup %{conn: conn} do
+      admin = insert(:user, is_admin: true)
+
+      %{conn: assign(conn, :user, admin), admin: admin}
+    end
+
+    test "it confirms emails of two users", %{admin: admin} do
+      [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+      assert first_user.confirmation_pending == true
+      assert second_user.confirmation_pending == true
+
+      build_conn()
+      |> assign(:user, admin)
+      |> patch("/api/pleroma/admin/users/confirm_email", %{
+        nicknames: [
+          first_user.nickname,
+          second_user.nickname
+        ]
+      })
+
+      assert first_user.confirmation_pending == true
+      assert second_user.confirmation_pending == true
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{
+                 second_user.nickname
+               }"
+    end
+  end
+
+  describe "PATCH /resend_confirmation_email" do
+    setup %{conn: conn} do
+      admin = insert(:user, is_admin: true)
+
+      %{conn: assign(conn, :user, admin), admin: admin}
+    end
+
+    test "it resend emails for two users", %{admin: admin} do
+      [first_user, second_user] = insert_pair(:user, confirmation_pending: true)
+
+      build_conn()
+      |> assign(:user, admin)
+      |> patch("/api/pleroma/admin/users/resend_confirmation_email", %{
+        nicknames: [
+          first_user.nickname,
+          second_user.nickname
+        ]
+      })
+
+      log_entry = Repo.one(ModerationLog)
+
+      assert ModerationLog.get_log_entry_message(log_entry) ==
+               "@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
+                 second_user.nickname
+               }"
+    end
+  end
 end
 
 # Needed for testing