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]
10 alias Pleroma.Activity
12 alias Pleroma.ConfigDB
14 alias Pleroma.ModerationLog
15 alias Pleroma.Plugs.OAuthScopesPlug
16 alias Pleroma.ReportNote
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Builder
21 alias Pleroma.Web.ActivityPub.Pipeline
22 alias Pleroma.Web.ActivityPub.Utils
23 alias Pleroma.Web.AdminAPI
24 alias Pleroma.Web.AdminAPI.AccountView
25 alias Pleroma.Web.AdminAPI.ConfigView
26 alias Pleroma.Web.AdminAPI.ModerationLogView
27 alias Pleroma.Web.AdminAPI.Report
28 alias Pleroma.Web.AdminAPI.ReportView
29 alias Pleroma.Web.AdminAPI.Search
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.Endpoint
32 alias Pleroma.Web.MastodonAPI
33 alias Pleroma.Web.Router
37 @descriptions Pleroma.Docs.JSON.compile()
42 %{scopes: ["read:accounts"], admin: true}
43 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
48 %{scopes: ["write:accounts"], admin: true}
51 :force_password_reset,
54 :user_toggle_activation,
63 :right_delete_multiple,
64 :update_user_credentials
70 %{scopes: ["write:follows"], admin: true}
71 when action in [:user_follow, :user_unfollow]
76 %{scopes: ["read:reports"], admin: true}
77 when action in [:list_reports, :report_show]
82 %{scopes: ["write:reports"], admin: true}
83 when action in [:reports_update, :report_notes_create, :report_notes_delete]
88 %{scopes: ["read:statuses"], admin: true}
89 when action in [:list_user_statuses, :list_instance_statuses]
94 %{scopes: ["read"], admin: true}
106 %{scopes: ["write"], admin: true}
110 :resend_confirmation_email,
116 action_fallback(AdminAPI.FallbackController)
118 def user_delete(conn, %{"nickname" => nickname}) do
119 user_delete(conn, %{"nicknames" => [nickname]})
122 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
125 |> Enum.map(&User.get_cached_by_nickname/1)
128 |> Enum.each(fn user ->
129 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
130 Pipeline.common_pipeline(delete_data, local: true)
133 ModerationLog.insert_log(%{
143 def user_follow(%{assigns: %{user: admin}} = conn, %{
144 "follower" => follower_nick,
145 "followed" => followed_nick
147 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
148 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
149 User.follow(follower, followed)
151 ModerationLog.insert_log(%{
163 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
164 "follower" => follower_nick,
165 "followed" => followed_nick
167 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
168 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
169 User.unfollow(follower, followed)
171 ModerationLog.insert_log(%{
183 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
185 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
191 password_confirmation: password,
195 User.register_changeset(%User{}, user_data, need_confirmation: false)
197 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
198 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
201 case Pleroma.Repo.transaction(changesets) do
206 |> Enum.map(fn user ->
207 {:ok, user} = User.post_register_action(user)
211 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
213 ModerationLog.insert_log(%{
215 subjects: Map.values(users),
222 {:error, id, changeset, _} ->
224 Enum.map(changesets.operations, fn
225 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
226 AccountView.render("create-error.json", %{changeset: changeset})
228 {_, {:changeset, current_changeset, _}} ->
229 AccountView.render("create-error.json", %{changeset: current_changeset})
233 |> put_status(:conflict)
238 def user_show(conn, %{"nickname" => nickname}) do
239 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
241 |> put_view(AccountView)
242 |> render("show.json", %{user: user})
244 _ -> {:error, :not_found}
248 def list_instance_statuses(conn, %{"instance" => instance} = params) do
249 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
250 {page, page_size} = page_params(params)
253 ActivityPub.fetch_statuses(nil, %{
254 "instance" => instance,
255 "limit" => page_size,
256 "offset" => (page - 1) * page_size,
257 "exclude_reblogs" => !with_reblogs && "true"
261 |> put_view(AdminAPI.StatusView)
262 |> render("index.json", %{activities: activities, as: :activity})
265 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
266 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
267 godmode = params["godmode"] == "true" || params["godmode"] == true
269 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
270 {_, page_size} = page_params(params)
273 ActivityPub.fetch_user_activities(user, nil, %{
274 "limit" => page_size,
275 "godmode" => godmode,
276 "exclude_reblogs" => !with_reblogs && "true"
280 |> put_view(MastodonAPI.StatusView)
281 |> render("index.json", %{activities: activities, as: :activity})
283 _ -> {:error, :not_found}
287 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
288 user = User.get_cached_by_nickname(nickname)
290 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
292 action = if user.deactivated, do: "activate", else: "deactivate"
294 ModerationLog.insert_log(%{
301 |> put_view(AccountView)
302 |> render("show.json", %{user: updated_user})
305 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
306 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
307 {:ok, updated_users} = User.deactivate(users, false)
309 ModerationLog.insert_log(%{
316 |> put_view(AccountView)
317 |> render("index.json", %{users: Keyword.values(updated_users)})
320 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
321 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
322 {:ok, updated_users} = User.deactivate(users, true)
324 ModerationLog.insert_log(%{
331 |> put_view(AccountView)
332 |> render("index.json", %{users: Keyword.values(updated_users)})
335 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
336 with {:ok, _} <- User.tag(nicknames, tags) do
337 ModerationLog.insert_log(%{
339 nicknames: nicknames,
344 json_response(conn, :no_content, "")
348 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
349 with {:ok, _} <- User.untag(nicknames, tags) do
350 ModerationLog.insert_log(%{
352 nicknames: nicknames,
357 json_response(conn, :no_content, "")
361 def list_users(conn, params) do
362 {page, page_size} = page_params(params)
363 filters = maybe_parse_filters(params["filters"])
366 query: params["query"],
368 page_size: page_size,
369 tags: params["tags"],
370 name: params["name"],
371 email: params["email"]
374 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
377 AccountView.render("index.json", users: users, count: count, page_size: page_size)
382 @filters ~w(local external active deactivated is_admin is_moderator)
384 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
385 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
387 defp maybe_parse_filters(filters) do
390 |> Enum.filter(&Enum.member?(@filters, &1))
391 |> Enum.map(&String.to_atom(&1))
392 |> Enum.into(%{}, &{&1, true})
395 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
396 "permission_group" => permission_group,
397 "nicknames" => nicknames
399 when permission_group in ["moderator", "admin"] do
400 update = %{:"is_#{permission_group}" => true}
402 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
404 for u <- users, do: User.admin_api_update(u, update)
406 ModerationLog.insert_log(%{
410 permission: permission_group
416 def right_add_multiple(conn, _) do
417 render_error(conn, :not_found, "No such permission_group")
420 def right_add(%{assigns: %{user: admin}} = conn, %{
421 "permission_group" => permission_group,
422 "nickname" => nickname
424 when permission_group in ["moderator", "admin"] do
425 fields = %{:"is_#{permission_group}" => true}
429 |> User.get_cached_by_nickname()
430 |> User.admin_api_update(fields)
432 ModerationLog.insert_log(%{
436 permission: permission_group
442 def right_add(conn, _) do
443 render_error(conn, :not_found, "No such permission_group")
446 def right_get(conn, %{"nickname" => nickname}) do
447 user = User.get_cached_by_nickname(nickname)
451 is_moderator: user.is_moderator,
452 is_admin: user.is_admin
456 def right_delete_multiple(
457 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
459 "permission_group" => permission_group,
460 "nicknames" => nicknames
463 when permission_group in ["moderator", "admin"] do
464 with false <- Enum.member?(nicknames, admin_nickname) do
465 update = %{:"is_#{permission_group}" => false}
467 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
469 for u <- users, do: User.admin_api_update(u, update)
471 ModerationLog.insert_log(%{
475 permission: permission_group
480 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
484 def right_delete_multiple(conn, _) do
485 render_error(conn, :not_found, "No such permission_group")
489 %{assigns: %{user: admin}} = conn,
491 "permission_group" => permission_group,
492 "nickname" => nickname
495 when permission_group in ["moderator", "admin"] do
496 fields = %{:"is_#{permission_group}" => false}
500 |> User.get_cached_by_nickname()
501 |> User.admin_api_update(fields)
503 ModerationLog.insert_log(%{
507 permission: permission_group
513 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
514 render_error(conn, :forbidden, "You can't revoke your own admin status.")
517 @doc "Get a password reset token (base64 string) for given nickname"
518 def get_password_reset(conn, %{"nickname" => nickname}) do
519 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
520 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
525 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
529 @doc "Force password reset for a given user"
530 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
531 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
533 Enum.each(users, &User.force_password_reset_async/1)
535 ModerationLog.insert_log(%{
538 action: "force_password_reset"
541 json_response(conn, :no_content, "")
544 @doc "Disable mfa for user's account."
545 def disable_mfa(conn, %{"nickname" => nickname}) do
546 case User.get_by_nickname(nickname) do
556 @doc "Show a given user's credentials"
557 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
558 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
560 |> put_view(AccountView)
561 |> render("credentials.json", %{user: user, for: admin})
563 _ -> {:error, :not_found}
567 @doc "Updates a given user"
568 def update_user_credentials(
569 %{assigns: %{user: admin}} = conn,
570 %{"nickname" => nickname} = params
572 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
574 User.update_as_admin(user, params) do
575 ModerationLog.insert_log(%{
578 action: "updated_users"
581 if params["password"] do
582 User.force_password_reset_async(user)
585 ModerationLog.insert_log(%{
588 action: "force_password_reset"
591 json(conn, %{status: "success"})
593 {:error, changeset} ->
594 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
596 json(conn, %{errors: errors})
599 json(conn, %{error: "Unable to update user."})
603 def list_reports(conn, params) do
604 {page, page_size} = page_params(params)
606 reports = Utils.get_reports(params, page, page_size)
609 |> put_view(ReportView)
610 |> render("index.json", %{reports: reports})
613 def report_show(conn, %{"id" => id}) do
614 with %Activity{} = report <- Activity.get_by_id(id) do
616 |> put_view(ReportView)
617 |> render("show.json", Report.extract_report_info(report))
619 _ -> {:error, :not_found}
623 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
626 |> Enum.map(fn report ->
627 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
628 ModerationLog.insert_log(%{
629 action: "report_update",
636 {:error, message} -> %{id: report["id"], error: message}
640 case Enum.any?(result, &Map.has_key?(&1, :error)) do
641 true -> json_response(conn, :bad_request, result)
642 false -> json_response(conn, :no_content, "")
646 def report_notes_create(%{assigns: %{user: user}} = conn, %{
650 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
651 ModerationLog.insert_log(%{
652 action: "report_note",
654 subject: Activity.get_by_id(report_id),
658 json_response(conn, :no_content, "")
660 _ -> json_response(conn, :bad_request, "")
664 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
666 "report_id" => report_id
668 with {:ok, note} <- ReportNote.destroy(note_id) do
669 ModerationLog.insert_log(%{
670 action: "report_note_delete",
672 subject: Activity.get_by_id(report_id),
676 json_response(conn, :no_content, "")
678 _ -> json_response(conn, :bad_request, "")
682 def list_log(conn, params) do
683 {page, page_size} = page_params(params)
686 ModerationLog.get_all(%{
688 page_size: page_size,
689 start_date: params["start_date"],
690 end_date: params["end_date"],
691 user_id: params["user_id"],
692 search: params["search"]
696 |> put_view(ModerationLogView)
697 |> render("index.json", %{log: log})
700 def config_descriptions(conn, _params) do
701 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
703 json(conn, descriptions)
706 def config_show(conn, %{"only_db" => true}) do
707 with :ok <- configurable_from_database() do
708 configs = Pleroma.Repo.all(ConfigDB)
711 |> put_view(ConfigView)
712 |> render("index.json", %{configs: configs})
716 def config_show(conn, _params) do
717 with :ok <- configurable_from_database() do
718 configs = ConfigDB.get_all_as_keyword()
721 Config.Holder.default_config()
722 |> ConfigDB.merge(configs)
723 |> Enum.map(fn {group, values} ->
724 Enum.map(values, fn {key, value} ->
726 if configs[group][key] do
727 ConfigDB.get_db_keys(configs[group][key], key)
730 db_value = configs[group][key]
733 if !is_nil(db_value) and Keyword.keyword?(db_value) and
734 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
735 ConfigDB.merge_group(group, key, value, db_value)
741 group: ConfigDB.convert(group),
742 key: ConfigDB.convert(key),
743 value: ConfigDB.convert(merged_value)
746 if db, do: Map.put(setting, :db, db), else: setting
751 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
755 def config_update(conn, %{"configs" => configs}) do
756 with :ok <- configurable_from_database() do
759 |> Enum.filter(&whitelisted_config?/1)
761 %{"group" => group, "key" => key, "delete" => true} = params ->
762 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
764 %{"group" => group, "key" => key, "value" => value} ->
765 ConfigDB.update_or_create(%{group: group, key: key, value: value})
767 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
771 |> Enum.map(fn {:ok, config} ->
772 Map.put(config, :db, ConfigDB.get_db_keys(config))
774 |> Enum.split_with(fn config ->
775 Ecto.get_meta(config, :state) == :deleted
778 Config.TransferTask.load_and_update_env(deleted, false)
780 if !Restarter.Pleroma.need_reboot?() do
781 changed_reboot_settings? =
783 |> Enum.any?(fn config ->
784 group = ConfigDB.from_string(config.group)
785 key = ConfigDB.from_string(config.key)
786 value = ConfigDB.from_binary(config.value)
787 Config.TransferTask.pleroma_need_restart?(group, key, value)
790 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
794 |> put_view(ConfigView)
795 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
799 def restart(conn, _params) do
800 with :ok <- configurable_from_database() do
801 Restarter.Pleroma.restart(Config.get(:env), 50)
807 def need_reboot(conn, _params) do
808 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
811 defp configurable_from_database do
812 if Config.get(:configurable_from_database) do
815 {:error, "To use this endpoint you need to enable configuration from database."}
819 defp whitelisted_config?(group, key) do
820 if whitelisted_configs = Config.get(:database_config_whitelist) do
821 Enum.any?(whitelisted_configs, fn
822 {whitelisted_group} ->
823 group == inspect(whitelisted_group)
825 {whitelisted_group, whitelisted_key} ->
826 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
833 defp whitelisted_config?(%{"group" => group, "key" => key}) do
834 whitelisted_config?(group, key)
837 defp whitelisted_config?(%{:group => group} = config) do
838 whitelisted_config?(group, config[:key])
841 def reload_emoji(conn, _params) do
842 Pleroma.Emoji.reload()
847 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
848 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
850 User.toggle_confirmation(users)
852 ModerationLog.insert_log(%{
855 action: "confirm_email"
861 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
862 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
864 User.try_send_confirmation_email(users)
866 ModerationLog.insert_log(%{
869 action: "resend_confirmation_email"
875 def stats(conn, _) do
876 count = Stats.get_status_visibility_count()
879 |> json(%{"status_visibility" => count})
882 defp page_params(params) do
883 {get_page(params["page"]), get_page_size(params["page_size"])}
886 defp get_page(page_string) when is_nil(page_string), do: 1
888 defp get_page(page_string) do
889 case Integer.parse(page_string) do
895 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
897 defp get_page_size(page_size_string) do
898 case Integer.parse(page_size_string) do
899 {page_size, _} -> page_size
900 :error -> @users_page_size