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.Relay
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.AdminAPI
25 alias Pleroma.Web.AdminAPI.AccountView
26 alias Pleroma.Web.AdminAPI.ConfigView
27 alias Pleroma.Web.AdminAPI.ModerationLogView
28 alias Pleroma.Web.AdminAPI.Report
29 alias Pleroma.Web.AdminAPI.ReportView
30 alias Pleroma.Web.AdminAPI.Search
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.Endpoint
33 alias Pleroma.Web.MastodonAPI
34 alias Pleroma.Web.MastodonAPI.AppView
35 alias Pleroma.Web.OAuth.App
36 alias Pleroma.Web.Router
40 @descriptions Pleroma.Docs.JSON.compile()
45 %{scopes: ["read:accounts"], admin: true}
46 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
51 %{scopes: ["write:accounts"], admin: true}
54 :force_password_reset,
57 :user_toggle_activation,
66 :right_delete_multiple,
67 :update_user_credentials
73 %{scopes: ["write:follows"], admin: true}
74 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
79 %{scopes: ["read:reports"], admin: true}
80 when action in [:list_reports, :report_show]
85 %{scopes: ["write:reports"], admin: true}
86 when action in [:reports_update, :report_notes_create, :report_notes_delete]
91 %{scopes: ["read:statuses"], admin: true}
92 when action in [:list_user_statuses, :list_instance_statuses]
97 %{scopes: ["read"], admin: true}
103 :config_descriptions,
110 %{scopes: ["write"], admin: true}
114 :resend_confirmation_email,
124 action_fallback(AdminAPI.FallbackController)
126 def user_delete(conn, %{"nickname" => nickname}) do
127 user_delete(conn, %{"nicknames" => [nickname]})
130 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
133 |> Enum.map(&User.get_cached_by_nickname/1)
136 |> Enum.each(fn user ->
137 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
138 Pipeline.common_pipeline(delete_data, local: true)
141 ModerationLog.insert_log(%{
151 def user_follow(%{assigns: %{user: admin}} = conn, %{
152 "follower" => follower_nick,
153 "followed" => followed_nick
155 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
156 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
157 User.follow(follower, followed)
159 ModerationLog.insert_log(%{
171 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
172 "follower" => follower_nick,
173 "followed" => followed_nick
175 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
176 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
177 User.unfollow(follower, followed)
179 ModerationLog.insert_log(%{
191 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
193 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
199 password_confirmation: password,
203 User.register_changeset(%User{}, user_data, need_confirmation: false)
205 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
206 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
209 case Pleroma.Repo.transaction(changesets) do
214 |> Enum.map(fn user ->
215 {:ok, user} = User.post_register_action(user)
219 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
221 ModerationLog.insert_log(%{
223 subjects: Map.values(users),
230 {:error, id, changeset, _} ->
232 Enum.map(changesets.operations, fn
233 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
234 AccountView.render("create-error.json", %{changeset: changeset})
236 {_, {:changeset, current_changeset, _}} ->
237 AccountView.render("create-error.json", %{changeset: current_changeset})
241 |> put_status(:conflict)
246 def user_show(conn, %{"nickname" => nickname}) do
247 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
249 |> put_view(AccountView)
250 |> render("show.json", %{user: user})
252 _ -> {:error, :not_found}
256 def list_instance_statuses(conn, %{"instance" => instance} = params) do
257 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
258 {page, page_size} = page_params(params)
261 ActivityPub.fetch_statuses(nil, %{
262 "instance" => instance,
263 "limit" => page_size,
264 "offset" => (page - 1) * page_size,
265 "exclude_reblogs" => !with_reblogs && "true"
269 |> put_view(AdminAPI.StatusView)
270 |> render("index.json", %{activities: activities, as: :activity})
273 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
274 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
275 godmode = params["godmode"] == "true" || params["godmode"] == true
277 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
278 {_, page_size} = page_params(params)
281 ActivityPub.fetch_user_activities(user, nil, %{
282 "limit" => page_size,
283 "godmode" => godmode,
284 "exclude_reblogs" => !with_reblogs && "true"
288 |> put_view(MastodonAPI.StatusView)
289 |> render("index.json", %{activities: activities, as: :activity})
291 _ -> {:error, :not_found}
295 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
296 user = User.get_cached_by_nickname(nickname)
298 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
300 action = if user.deactivated, do: "activate", else: "deactivate"
302 ModerationLog.insert_log(%{
309 |> put_view(AccountView)
310 |> render("show.json", %{user: updated_user})
313 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
314 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
315 {:ok, updated_users} = User.deactivate(users, false)
317 ModerationLog.insert_log(%{
324 |> put_view(AccountView)
325 |> render("index.json", %{users: Keyword.values(updated_users)})
328 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
329 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
330 {:ok, updated_users} = User.deactivate(users, true)
332 ModerationLog.insert_log(%{
339 |> put_view(AccountView)
340 |> render("index.json", %{users: Keyword.values(updated_users)})
343 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
344 with {:ok, _} <- User.tag(nicknames, tags) do
345 ModerationLog.insert_log(%{
347 nicknames: nicknames,
352 json_response(conn, :no_content, "")
356 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
357 with {:ok, _} <- User.untag(nicknames, tags) do
358 ModerationLog.insert_log(%{
360 nicknames: nicknames,
365 json_response(conn, :no_content, "")
369 def list_users(conn, params) do
370 {page, page_size} = page_params(params)
371 filters = maybe_parse_filters(params["filters"])
374 query: params["query"],
376 page_size: page_size,
377 tags: params["tags"],
378 name: params["name"],
379 email: params["email"]
382 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
385 AccountView.render("index.json", users: users, count: count, page_size: page_size)
390 @filters ~w(local external active deactivated is_admin is_moderator)
392 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
393 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
395 defp maybe_parse_filters(filters) do
398 |> Enum.filter(&Enum.member?(@filters, &1))
399 |> Enum.map(&String.to_atom(&1))
400 |> Enum.into(%{}, &{&1, true})
403 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
404 "permission_group" => permission_group,
405 "nicknames" => nicknames
407 when permission_group in ["moderator", "admin"] do
408 update = %{:"is_#{permission_group}" => true}
410 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
412 for u <- users, do: User.admin_api_update(u, update)
414 ModerationLog.insert_log(%{
418 permission: permission_group
424 def right_add_multiple(conn, _) do
425 render_error(conn, :not_found, "No such permission_group")
428 def right_add(%{assigns: %{user: admin}} = conn, %{
429 "permission_group" => permission_group,
430 "nickname" => nickname
432 when permission_group in ["moderator", "admin"] do
433 fields = %{:"is_#{permission_group}" => true}
437 |> User.get_cached_by_nickname()
438 |> User.admin_api_update(fields)
440 ModerationLog.insert_log(%{
444 permission: permission_group
450 def right_add(conn, _) do
451 render_error(conn, :not_found, "No such permission_group")
454 def right_get(conn, %{"nickname" => nickname}) do
455 user = User.get_cached_by_nickname(nickname)
459 is_moderator: user.is_moderator,
460 is_admin: user.is_admin
464 def right_delete_multiple(
465 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
467 "permission_group" => permission_group,
468 "nicknames" => nicknames
471 when permission_group in ["moderator", "admin"] do
472 with false <- Enum.member?(nicknames, admin_nickname) do
473 update = %{:"is_#{permission_group}" => false}
475 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
477 for u <- users, do: User.admin_api_update(u, update)
479 ModerationLog.insert_log(%{
483 permission: permission_group
488 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
492 def right_delete_multiple(conn, _) do
493 render_error(conn, :not_found, "No such permission_group")
497 %{assigns: %{user: admin}} = conn,
499 "permission_group" => permission_group,
500 "nickname" => nickname
503 when permission_group in ["moderator", "admin"] do
504 fields = %{:"is_#{permission_group}" => false}
508 |> User.get_cached_by_nickname()
509 |> User.admin_api_update(fields)
511 ModerationLog.insert_log(%{
515 permission: permission_group
521 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
522 render_error(conn, :forbidden, "You can't revoke your own admin status.")
525 def relay_list(conn, _params) do
526 with {:ok, list} <- Relay.list() do
527 json(conn, %{relays: list})
535 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
536 with {:ok, _message} <- Relay.follow(target) do
537 ModerationLog.insert_log(%{
538 action: "relay_follow",
552 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
553 with {:ok, _message} <- Relay.unfollow(target) do
554 ModerationLog.insert_log(%{
555 action: "relay_unfollow",
569 @doc "Get a password reset token (base64 string) for given nickname"
570 def get_password_reset(conn, %{"nickname" => nickname}) do
571 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
572 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
577 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
581 @doc "Force password reset for a given user"
582 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
583 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
585 Enum.each(users, &User.force_password_reset_async/1)
587 ModerationLog.insert_log(%{
590 action: "force_password_reset"
593 json_response(conn, :no_content, "")
596 @doc "Disable mfa for user's account."
597 def disable_mfa(conn, %{"nickname" => nickname}) do
598 case User.get_by_nickname(nickname) do
608 @doc "Show a given user's credentials"
609 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
610 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
612 |> put_view(AccountView)
613 |> render("credentials.json", %{user: user, for: admin})
615 _ -> {:error, :not_found}
619 @doc "Updates a given user"
620 def update_user_credentials(
621 %{assigns: %{user: admin}} = conn,
622 %{"nickname" => nickname} = params
624 with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
626 User.update_as_admin(user, params) do
627 ModerationLog.insert_log(%{
630 action: "updated_users"
633 if params["password"] do
634 User.force_password_reset_async(user)
637 ModerationLog.insert_log(%{
640 action: "force_password_reset"
643 json(conn, %{status: "success"})
645 {:error, changeset} ->
646 {_, {error, _}} = Enum.at(changeset.errors, 0)
647 json(conn, %{error: "New password #{error}."})
650 json(conn, %{error: "Unable to change password."})
654 def list_reports(conn, params) do
655 {page, page_size} = page_params(params)
657 reports = Utils.get_reports(params, page, page_size)
660 |> put_view(ReportView)
661 |> render("index.json", %{reports: reports})
664 def report_show(conn, %{"id" => id}) do
665 with %Activity{} = report <- Activity.get_by_id(id) do
667 |> put_view(ReportView)
668 |> render("show.json", Report.extract_report_info(report))
670 _ -> {:error, :not_found}
674 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
677 |> Enum.map(fn report ->
678 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
679 ModerationLog.insert_log(%{
680 action: "report_update",
687 {:error, message} -> %{id: report["id"], error: message}
691 case Enum.any?(result, &Map.has_key?(&1, :error)) do
692 true -> json_response(conn, :bad_request, result)
693 false -> json_response(conn, :no_content, "")
697 def report_notes_create(%{assigns: %{user: user}} = conn, %{
701 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
702 ModerationLog.insert_log(%{
703 action: "report_note",
705 subject: Activity.get_by_id(report_id),
709 json_response(conn, :no_content, "")
711 _ -> json_response(conn, :bad_request, "")
715 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
717 "report_id" => report_id
719 with {:ok, note} <- ReportNote.destroy(note_id) do
720 ModerationLog.insert_log(%{
721 action: "report_note_delete",
723 subject: Activity.get_by_id(report_id),
727 json_response(conn, :no_content, "")
729 _ -> json_response(conn, :bad_request, "")
733 def list_log(conn, params) do
734 {page, page_size} = page_params(params)
737 ModerationLog.get_all(%{
739 page_size: page_size,
740 start_date: params["start_date"],
741 end_date: params["end_date"],
742 user_id: params["user_id"],
743 search: params["search"]
747 |> put_view(ModerationLogView)
748 |> render("index.json", %{log: log})
751 def config_descriptions(conn, _params) do
752 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
754 json(conn, descriptions)
757 def config_show(conn, %{"only_db" => true}) do
758 with :ok <- configurable_from_database() do
759 configs = Pleroma.Repo.all(ConfigDB)
762 |> put_view(ConfigView)
763 |> render("index.json", %{configs: configs})
767 def config_show(conn, _params) do
768 with :ok <- configurable_from_database() do
769 configs = ConfigDB.get_all_as_keyword()
772 Config.Holder.default_config()
773 |> ConfigDB.merge(configs)
774 |> Enum.map(fn {group, values} ->
775 Enum.map(values, fn {key, value} ->
777 if configs[group][key] do
778 ConfigDB.get_db_keys(configs[group][key], key)
781 db_value = configs[group][key]
784 if !is_nil(db_value) and Keyword.keyword?(db_value) and
785 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
786 ConfigDB.merge_group(group, key, value, db_value)
792 group: ConfigDB.convert(group),
793 key: ConfigDB.convert(key),
794 value: ConfigDB.convert(merged_value)
797 if db, do: Map.put(setting, :db, db), else: setting
802 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
806 def config_update(conn, %{"configs" => configs}) do
807 with :ok <- configurable_from_database() do
810 |> Enum.filter(&whitelisted_config?/1)
812 %{"group" => group, "key" => key, "delete" => true} = params ->
813 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
815 %{"group" => group, "key" => key, "value" => value} ->
816 ConfigDB.update_or_create(%{group: group, key: key, value: value})
818 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
822 |> Enum.map(fn {:ok, config} ->
823 Map.put(config, :db, ConfigDB.get_db_keys(config))
825 |> Enum.split_with(fn config ->
826 Ecto.get_meta(config, :state) == :deleted
829 Config.TransferTask.load_and_update_env(deleted, false)
831 if !Restarter.Pleroma.need_reboot?() do
832 changed_reboot_settings? =
834 |> Enum.any?(fn config ->
835 group = ConfigDB.from_string(config.group)
836 key = ConfigDB.from_string(config.key)
837 value = ConfigDB.from_binary(config.value)
838 Config.TransferTask.pleroma_need_restart?(group, key, value)
841 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
845 |> put_view(ConfigView)
846 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
850 def restart(conn, _params) do
851 with :ok <- configurable_from_database() do
852 Restarter.Pleroma.restart(Config.get(:env), 50)
858 def need_reboot(conn, _params) do
859 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
862 defp configurable_from_database do
863 if Config.get(:configurable_from_database) do
866 {:error, "To use this endpoint you need to enable configuration from database."}
870 defp whitelisted_config?(group, key) do
871 if whitelisted_configs = Config.get(:database_config_whitelist) do
872 Enum.any?(whitelisted_configs, fn
873 {whitelisted_group} ->
874 group == inspect(whitelisted_group)
876 {whitelisted_group, whitelisted_key} ->
877 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
884 defp whitelisted_config?(%{"group" => group, "key" => key}) do
885 whitelisted_config?(group, key)
888 defp whitelisted_config?(%{:group => group} = config) do
889 whitelisted_config?(group, config[:key])
892 def reload_emoji(conn, _params) do
893 Pleroma.Emoji.reload()
898 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
899 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
901 User.toggle_confirmation(users)
903 ModerationLog.insert_log(%{
906 action: "confirm_email"
912 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
913 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
915 User.try_send_confirmation_email(users)
917 ModerationLog.insert_log(%{
920 action: "resend_confirmation_email"
926 def oauth_app_create(conn, params) do
929 Map.put(params, "client_name", params["name"])
935 case App.create(params) do
937 AppView.render("show.json", %{app: app, admin: true})
939 {:error, changeset} ->
940 App.errors(changeset)
946 def oauth_app_update(conn, params) do
949 Map.put(params, "client_name", params["name"])
954 with {:ok, app} <- App.update(params) do
955 json(conn, AppView.render("show.json", %{app: app, admin: true}))
957 {:error, changeset} ->
958 json(conn, App.errors(changeset))
961 json_response(conn, :bad_request, "")
965 def oauth_app_list(conn, params) do
966 {page, page_size} = page_params(params)
969 client_name: params["name"],
970 client_id: params["client_id"],
976 if Map.has_key?(params, "trusted") do
977 Map.put(search_params, :trusted, params["trusted"])
982 with {:ok, apps, count} <- App.search(search_params) do
985 AppView.render("index.json",
988 page_size: page_size,
995 def oauth_app_delete(conn, params) do
996 with {:ok, _app} <- App.destroy(params["id"]) do
997 json_response(conn, :no_content, "")
999 _ -> json_response(conn, :bad_request, "")
1003 def stats(conn, _) do
1004 count = Stats.get_status_visibility_count()
1007 |> json(%{"status_visibility" => count})
1010 defp page_params(params) do
1011 {get_page(params["page"]), get_page_size(params["page_size"])}
1014 defp get_page(page_string) when is_nil(page_string), do: 1
1016 defp get_page(page_string) do
1017 case Integer.parse(page_string) do
1023 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1025 defp get_page_size(page_size_string) do
1026 case Integer.parse(page_size_string) do
1027 {page_size, _} -> page_size
1028 :error -> @users_page_size