Merge branch 'feature/confirm-user-acc-resend-confirmation' into 'develop'

AdminAPI: Confirm user account, resend confirmation email

See merge request pleroma/pleroma!1994
This commit is contained in:
lain 2019-11-22 15:39:19 +00:00
commit bd62946020
8 changed files with 197 additions and 30 deletions

View file

@ -70,6 +70,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

View file

@ -870,3 +870,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

View file

@ -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

View file

@ -491,6 +491,10 @@ def try_send_confirmation_email(%User{} = user) do
end
end
def try_send_confirmation_email(users) do
Enum.each(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
@ -1582,6 +1586,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

View file

@ -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)

View file

@ -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

View file

@ -178,6 +178,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)

View file

@ -225,7 +225,8 @@ test "Show", %{conn: conn} do
"roles" => %{"admin" => false, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
assert expected == json_response(conn, 200)
@ -634,7 +635,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname)
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => user.deactivated,
@ -644,7 +646,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
"local" => false,
"tags" => ["foo", "bar"],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@ -685,7 +688,8 @@ test "regular search", %{conn: conn} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -709,7 +713,8 @@ test "search by domain", %{conn: conn} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -733,7 +738,8 @@ test "search by full nickname", %{conn: conn} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -757,7 +763,8 @@ test "search by display name", %{conn: conn} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -781,7 +788,8 @@ test "search by email", %{conn: conn} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -805,7 +813,8 @@ test "regular search with page size", %{conn: conn} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -824,7 +833,8 @@ test "regular search with page size", %{conn: conn} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname)
"display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
}
]
}
@ -853,7 +863,8 @@ test "only local users" do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -880,7 +891,8 @@ test "only local users with no query", %{admin: old_admin} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => admin.deactivated,
@ -890,7 +902,8 @@ test "only local users with no query", %{admin: old_admin} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname)
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
@ -900,7 +913,8 @@ test "only local users with no query", %{admin: old_admin} do
"roles" => %{"admin" => true, "moderator" => false},
"tags" => [],
"avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname)
"display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@ -929,7 +943,8 @@ test "load only admins", %{conn: conn, admin: admin} do
"local" => admin.local,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname)
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
@ -939,7 +954,8 @@ test "load only admins", %{conn: conn, admin: admin} do
"local" => second_admin.local,
"tags" => [],
"avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname)
"display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@ -970,7 +986,8 @@ test "load only moderators", %{conn: conn} do
"local" => moderator.local,
"tags" => [],
"avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(moderator.name || moderator.nickname)
"display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
"confirmation_pending" => false
}
]
}
@ -994,7 +1011,8 @@ test "load users with tags list", %{conn: conn} do
"local" => user1.local,
"tags" => ["first"],
"avatar" => User.avatar_url(user1) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user1.name || user1.nickname)
"display_name" => HTML.strip_tags(user1.name || user1.nickname),
"confirmation_pending" => false
},
%{
"deactivated" => false,
@ -1004,7 +1022,8 @@ test "load users with tags list", %{conn: conn} do
"local" => user2.local,
"tags" => ["second"],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname)
"display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
}
]
|> Enum.sort_by(& &1["nickname"])
@ -1040,7 +1059,8 @@ test "it works with multiple filters" do
"local" => user.local,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
]
}
@ -1066,7 +1086,8 @@ test "it omits relay user", %{admin: admin} do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname)
"display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
}
]
}
@ -1135,7 +1156,8 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
"local" => true,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname)
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}
log_entry = Repo.one(ModerationLog)
@ -2839,6 +2861,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