1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.AdminAPI.AdminAPIController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
11 alias Pleroma.ConfigDB
13 alias Pleroma.ModerationLog
14 alias Pleroma.Plugs.OAuthScopesPlug
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Builder
19 alias Pleroma.Web.ActivityPub.Pipeline
20 alias Pleroma.Web.ActivityPub.Relay
21 alias Pleroma.Web.AdminAPI
22 alias Pleroma.Web.AdminAPI.AccountView
23 alias Pleroma.Web.AdminAPI.ConfigView
24 alias Pleroma.Web.AdminAPI.ModerationLogView
25 alias Pleroma.Web.AdminAPI.Search
26 alias Pleroma.Web.Endpoint
27 alias Pleroma.Web.Router
31 @descriptions Pleroma.Docs.JSON.compile()
36 %{scopes: ["read:accounts"], admin: true}
37 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
42 %{scopes: ["write:accounts"], admin: true}
45 :force_password_reset,
48 :user_toggle_activation,
57 :right_delete_multiple,
58 :update_user_credentials
64 %{scopes: ["write:follows"], admin: true}
65 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
70 %{scopes: ["read:statuses"], admin: true}
71 when action in [:list_user_statuses, :list_instance_statuses]
76 %{scopes: ["read"], admin: true}
89 %{scopes: ["write"], admin: true}
93 :resend_confirmation_email,
99 action_fallback(AdminAPI.FallbackController)
101 def user_delete(conn, %{"nickname" => nickname}) do
102 user_delete(conn, %{"nicknames" => [nickname]})
105 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
108 |> Enum.map(&User.get_cached_by_nickname/1)
111 |> Enum.each(fn user ->
112 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
113 Pipeline.common_pipeline(delete_data, local: true)
116 ModerationLog.insert_log(%{
126 def user_follow(%{assigns: %{user: admin}} = conn, %{
127 "follower" => follower_nick,
128 "followed" => followed_nick
130 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
131 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
132 User.follow(follower, followed)
134 ModerationLog.insert_log(%{
146 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
147 "follower" => follower_nick,
148 "followed" => followed_nick
150 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
151 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
152 User.unfollow(follower, followed)
154 ModerationLog.insert_log(%{
166 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
168 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
174 password_confirmation: password,
178 User.register_changeset(%User{}, user_data, need_confirmation: false)
180 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
181 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
184 case Pleroma.Repo.transaction(changesets) do
189 |> Enum.map(fn user ->
190 {:ok, user} = User.post_register_action(user)
194 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
196 ModerationLog.insert_log(%{
198 subjects: Map.values(users),
205 {:error, id, changeset, _} ->
207 Enum.map(changesets.operations, fn
208 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
209 AccountView.render("create-error.json", %{changeset: changeset})
211 {_, {:changeset, current_changeset, _}} ->
212 AccountView.render("create-error.json", %{changeset: current_changeset})
216 |> put_status(:conflict)
221 def user_show(conn, %{"nickname" => nickname}) do
222 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
224 |> put_view(AccountView)
225 |> render("show.json", %{user: user})
227 _ -> {:error, :not_found}
231 def list_instance_statuses(conn, %{"instance" => instance} = params) do
232 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
233 {page, page_size} = page_params(params)
236 ActivityPub.fetch_statuses(nil, %{
237 "instance" => instance,
238 "limit" => page_size,
239 "offset" => (page - 1) * page_size,
240 "exclude_reblogs" => !with_reblogs && "true"
244 |> put_view(AdminAPI.StatusView)
245 |> render("index.json", %{activities: activities, as: :activity})
248 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
249 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
250 godmode = params["godmode"] == "true" || params["godmode"] == true
252 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
253 {_, page_size} = page_params(params)
256 ActivityPub.fetch_user_activities(user, nil, %{
257 "limit" => page_size,
258 "godmode" => godmode,
259 "exclude_reblogs" => !with_reblogs && "true"
263 |> put_view(AdminAPI.StatusView)
264 |> render("index.json", %{activities: activities, as: :activity})
266 _ -> {:error, :not_found}
270 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
271 user = User.get_cached_by_nickname(nickname)
273 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
275 action = if user.deactivated, do: "activate", else: "deactivate"
277 ModerationLog.insert_log(%{
284 |> put_view(AccountView)
285 |> render("show.json", %{user: updated_user})
288 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
289 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
290 {:ok, updated_users} = User.deactivate(users, false)
292 ModerationLog.insert_log(%{
299 |> put_view(AccountView)
300 |> render("index.json", %{users: Keyword.values(updated_users)})
303 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
304 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
305 {:ok, updated_users} = User.deactivate(users, true)
307 ModerationLog.insert_log(%{
314 |> put_view(AccountView)
315 |> render("index.json", %{users: Keyword.values(updated_users)})
318 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
319 with {:ok, _} <- User.tag(nicknames, tags) do
320 ModerationLog.insert_log(%{
322 nicknames: nicknames,
327 json_response(conn, :no_content, "")
331 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
332 with {:ok, _} <- User.untag(nicknames, tags) do
333 ModerationLog.insert_log(%{
335 nicknames: nicknames,
340 json_response(conn, :no_content, "")
344 def list_users(conn, params) do
345 {page, page_size} = page_params(params)
346 filters = maybe_parse_filters(params["filters"])
349 query: params["query"],
351 page_size: page_size,
352 tags: params["tags"],
353 name: params["name"],
354 email: params["email"]
357 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
360 AccountView.render("index.json", users: users, count: count, page_size: page_size)
365 @filters ~w(local external active deactivated is_admin is_moderator)
367 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
368 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
370 defp maybe_parse_filters(filters) do
373 |> Enum.filter(&Enum.member?(@filters, &1))
374 |> Enum.map(&String.to_atom(&1))
375 |> Enum.into(%{}, &{&1, true})
378 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
379 "permission_group" => permission_group,
380 "nicknames" => nicknames
382 when permission_group in ["moderator", "admin"] do
383 update = %{:"is_#{permission_group}" => true}
385 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
387 for u <- users, do: User.admin_api_update(u, update)
389 ModerationLog.insert_log(%{
393 permission: permission_group
399 def right_add_multiple(conn, _) do
400 render_error(conn, :not_found, "No such permission_group")
403 def right_add(%{assigns: %{user: admin}} = conn, %{
404 "permission_group" => permission_group,
405 "nickname" => nickname
407 when permission_group in ["moderator", "admin"] do
408 fields = %{:"is_#{permission_group}" => true}
412 |> User.get_cached_by_nickname()
413 |> User.admin_api_update(fields)
415 ModerationLog.insert_log(%{
419 permission: permission_group
425 def right_add(conn, _) do
426 render_error(conn, :not_found, "No such permission_group")
429 def right_get(conn, %{"nickname" => nickname}) do
430 user = User.get_cached_by_nickname(nickname)
434 is_moderator: user.is_moderator,
435 is_admin: user.is_admin
439 def right_delete_multiple(
440 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
442 "permission_group" => permission_group,
443 "nicknames" => nicknames
446 when permission_group in ["moderator", "admin"] do
447 with false <- Enum.member?(nicknames, admin_nickname) do
448 update = %{:"is_#{permission_group}" => false}
450 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
452 for u <- users, do: User.admin_api_update(u, update)
454 ModerationLog.insert_log(%{
458 permission: permission_group
463 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
467 def right_delete_multiple(conn, _) do
468 render_error(conn, :not_found, "No such permission_group")
472 %{assigns: %{user: admin}} = conn,
474 "permission_group" => permission_group,
475 "nickname" => nickname
478 when permission_group in ["moderator", "admin"] do
479 fields = %{:"is_#{permission_group}" => false}
483 |> User.get_cached_by_nickname()
484 |> User.admin_api_update(fields)
486 ModerationLog.insert_log(%{
490 permission: permission_group
496 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
497 render_error(conn, :forbidden, "You can't revoke your own admin status.")
500 def relay_list(conn, _params) do
501 with {:ok, list} <- Relay.list() do
502 json(conn, %{relays: list})
510 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
511 with {:ok, _message} <- Relay.follow(target) do
512 ModerationLog.insert_log(%{
513 action: "relay_follow",
527 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
528 with {:ok, _message} <- Relay.unfollow(target) do
529 ModerationLog.insert_log(%{
530 action: "relay_unfollow",
544 @doc "Get a password reset token (base64 string) for given nickname"
545 def get_password_reset(conn, %{"nickname" => nickname}) do
546 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
547 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
552 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
556 @doc "Force password reset for a given user"
557 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
558 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
560 Enum.each(users, &User.force_password_reset_async/1)
562 ModerationLog.insert_log(%{
565 action: "force_password_reset"
568 json_response(conn, :no_content, "")
571 @doc "Disable mfa for user's account."
572 def disable_mfa(conn, %{"nickname" => nickname}) do
573 case User.get_by_nickname(nickname) do
583 @doc "Show a given user's credentials"
584 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
585 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
587 |> put_view(AccountView)
588 |> render("credentials.json", %{user: user, for: admin})
590 _ -> {:error, :not_found}
594 @doc "Updates a given user"
595 def update_user_credentials(
596 %{assigns: %{user: admin}} = conn,
597 %{"nickname" => nickname} = params
599 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
601 User.update_as_admin(user, params) do
602 ModerationLog.insert_log(%{
605 action: "updated_users"
608 if params["password"] do
609 User.force_password_reset_async(user)
612 ModerationLog.insert_log(%{
615 action: "force_password_reset"
618 json(conn, %{status: "success"})
620 {:error, changeset} ->
621 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
623 json(conn, %{errors: errors})
626 json(conn, %{error: "Unable to update user."})
630 def list_log(conn, params) do
631 {page, page_size} = page_params(params)
634 ModerationLog.get_all(%{
636 page_size: page_size,
637 start_date: params["start_date"],
638 end_date: params["end_date"],
639 user_id: params["user_id"],
640 search: params["search"]
644 |> put_view(ModerationLogView)
645 |> render("index.json", %{log: log})
648 def config_descriptions(conn, _params) do
649 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
651 json(conn, descriptions)
654 def config_show(conn, %{"only_db" => true}) do
655 with :ok <- configurable_from_database() do
656 configs = Pleroma.Repo.all(ConfigDB)
659 |> put_view(ConfigView)
660 |> render("index.json", %{configs: configs})
664 def config_show(conn, _params) do
665 with :ok <- configurable_from_database() do
666 configs = ConfigDB.get_all_as_keyword()
669 Config.Holder.default_config()
670 |> ConfigDB.merge(configs)
671 |> Enum.map(fn {group, values} ->
672 Enum.map(values, fn {key, value} ->
674 if configs[group][key] do
675 ConfigDB.get_db_keys(configs[group][key], key)
678 db_value = configs[group][key]
681 if !is_nil(db_value) and Keyword.keyword?(db_value) and
682 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
683 ConfigDB.merge_group(group, key, value, db_value)
689 group: ConfigDB.convert(group),
690 key: ConfigDB.convert(key),
691 value: ConfigDB.convert(merged_value)
694 if db, do: Map.put(setting, :db, db), else: setting
699 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
703 def config_update(conn, %{"configs" => configs}) do
704 with :ok <- configurable_from_database() do
707 |> Enum.filter(&whitelisted_config?/1)
709 %{"group" => group, "key" => key, "delete" => true} = params ->
710 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
712 %{"group" => group, "key" => key, "value" => value} ->
713 ConfigDB.update_or_create(%{group: group, key: key, value: value})
715 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
719 |> Enum.map(fn {:ok, config} ->
720 Map.put(config, :db, ConfigDB.get_db_keys(config))
722 |> Enum.split_with(fn config ->
723 Ecto.get_meta(config, :state) == :deleted
726 Config.TransferTask.load_and_update_env(deleted, false)
728 if !Restarter.Pleroma.need_reboot?() do
729 changed_reboot_settings? =
731 |> Enum.any?(fn config ->
732 group = ConfigDB.from_string(config.group)
733 key = ConfigDB.from_string(config.key)
734 value = ConfigDB.from_binary(config.value)
735 Config.TransferTask.pleroma_need_restart?(group, key, value)
738 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
742 |> put_view(ConfigView)
743 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
747 def restart(conn, _params) do
748 with :ok <- configurable_from_database() do
749 Restarter.Pleroma.restart(Config.get(:env), 50)
755 def need_reboot(conn, _params) do
756 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
759 defp configurable_from_database do
760 if Config.get(:configurable_from_database) do
763 {:error, "To use this endpoint you need to enable configuration from database."}
767 defp whitelisted_config?(group, key) do
768 if whitelisted_configs = Config.get(:database_config_whitelist) do
769 Enum.any?(whitelisted_configs, fn
770 {whitelisted_group} ->
771 group == inspect(whitelisted_group)
773 {whitelisted_group, whitelisted_key} ->
774 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
781 defp whitelisted_config?(%{"group" => group, "key" => key}) do
782 whitelisted_config?(group, key)
785 defp whitelisted_config?(%{:group => group} = config) do
786 whitelisted_config?(group, config[:key])
789 def reload_emoji(conn, _params) do
790 Pleroma.Emoji.reload()
795 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
796 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
798 User.toggle_confirmation(users)
800 ModerationLog.insert_log(%{
803 action: "confirm_email"
809 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
810 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
812 User.try_send_confirmation_email(users)
814 ModerationLog.insert_log(%{
817 action: "resend_confirmation_email"
823 def stats(conn, _) do
824 count = Stats.get_status_visibility_count()
827 |> json(%{"status_visibility" => count})
830 defp page_params(params) do
831 {get_page(params["page"]), get_page_size(params["page_size"])}
834 defp get_page(page_string) when is_nil(page_string), do: 1
836 defp get_page(page_string) do
837 case Integer.parse(page_string) do
843 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
845 defp get_page_size(page_size_string) do
846 case Integer.parse(page_size_string) do
847 {page_size, _} -> page_size
848 :error -> @users_page_size