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, 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 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
648 json(conn, %{errors: errors})
651 json(conn, %{error: "Unable to update user."})
655 def list_reports(conn, params) do
656 {page, page_size} = page_params(params)
658 reports = Utils.get_reports(params, page, page_size)
661 |> put_view(ReportView)
662 |> render("index.json", %{reports: reports})
665 def report_show(conn, %{"id" => id}) do
666 with %Activity{} = report <- Activity.get_by_id(id) do
668 |> put_view(ReportView)
669 |> render("show.json", Report.extract_report_info(report))
671 _ -> {:error, :not_found}
675 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
678 |> Enum.map(fn report ->
679 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
680 ModerationLog.insert_log(%{
681 action: "report_update",
688 {:error, message} -> %{id: report["id"], error: message}
692 case Enum.any?(result, &Map.has_key?(&1, :error)) do
693 true -> json_response(conn, :bad_request, result)
694 false -> json_response(conn, :no_content, "")
698 def report_notes_create(%{assigns: %{user: user}} = conn, %{
702 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
703 ModerationLog.insert_log(%{
704 action: "report_note",
706 subject: Activity.get_by_id(report_id),
710 json_response(conn, :no_content, "")
712 _ -> json_response(conn, :bad_request, "")
716 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
718 "report_id" => report_id
720 with {:ok, note} <- ReportNote.destroy(note_id) do
721 ModerationLog.insert_log(%{
722 action: "report_note_delete",
724 subject: Activity.get_by_id(report_id),
728 json_response(conn, :no_content, "")
730 _ -> json_response(conn, :bad_request, "")
734 def list_log(conn, params) do
735 {page, page_size} = page_params(params)
738 ModerationLog.get_all(%{
740 page_size: page_size,
741 start_date: params["start_date"],
742 end_date: params["end_date"],
743 user_id: params["user_id"],
744 search: params["search"]
748 |> put_view(ModerationLogView)
749 |> render("index.json", %{log: log})
752 def config_descriptions(conn, _params) do
753 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
755 json(conn, descriptions)
758 def config_show(conn, %{"only_db" => true}) do
759 with :ok <- configurable_from_database() do
760 configs = Pleroma.Repo.all(ConfigDB)
763 |> put_view(ConfigView)
764 |> render("index.json", %{configs: configs})
768 def config_show(conn, _params) do
769 with :ok <- configurable_from_database() do
770 configs = ConfigDB.get_all_as_keyword()
773 Config.Holder.default_config()
774 |> ConfigDB.merge(configs)
775 |> Enum.map(fn {group, values} ->
776 Enum.map(values, fn {key, value} ->
778 if configs[group][key] do
779 ConfigDB.get_db_keys(configs[group][key], key)
782 db_value = configs[group][key]
785 if !is_nil(db_value) and Keyword.keyword?(db_value) and
786 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
787 ConfigDB.merge_group(group, key, value, db_value)
793 group: ConfigDB.convert(group),
794 key: ConfigDB.convert(key),
795 value: ConfigDB.convert(merged_value)
798 if db, do: Map.put(setting, :db, db), else: setting
803 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
807 def config_update(conn, %{"configs" => configs}) do
808 with :ok <- configurable_from_database() do
811 |> Enum.filter(&whitelisted_config?/1)
813 %{"group" => group, "key" => key, "delete" => true} = params ->
814 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
816 %{"group" => group, "key" => key, "value" => value} ->
817 ConfigDB.update_or_create(%{group: group, key: key, value: value})
819 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
823 |> Enum.map(fn {:ok, config} ->
824 Map.put(config, :db, ConfigDB.get_db_keys(config))
826 |> Enum.split_with(fn config ->
827 Ecto.get_meta(config, :state) == :deleted
830 Config.TransferTask.load_and_update_env(deleted, false)
832 if !Restarter.Pleroma.need_reboot?() do
833 changed_reboot_settings? =
835 |> Enum.any?(fn config ->
836 group = ConfigDB.from_string(config.group)
837 key = ConfigDB.from_string(config.key)
838 value = ConfigDB.from_binary(config.value)
839 Config.TransferTask.pleroma_need_restart?(group, key, value)
842 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
846 |> put_view(ConfigView)
847 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
851 def restart(conn, _params) do
852 with :ok <- configurable_from_database() do
853 Restarter.Pleroma.restart(Config.get(:env), 50)
859 def need_reboot(conn, _params) do
860 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
863 defp configurable_from_database do
864 if Config.get(:configurable_from_database) do
867 {:error, "To use this endpoint you need to enable configuration from database."}
871 defp whitelisted_config?(group, key) do
872 if whitelisted_configs = Config.get(:database_config_whitelist) do
873 Enum.any?(whitelisted_configs, fn
874 {whitelisted_group} ->
875 group == inspect(whitelisted_group)
877 {whitelisted_group, whitelisted_key} ->
878 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
885 defp whitelisted_config?(%{"group" => group, "key" => key}) do
886 whitelisted_config?(group, key)
889 defp whitelisted_config?(%{:group => group} = config) do
890 whitelisted_config?(group, config[:key])
893 def reload_emoji(conn, _params) do
894 Pleroma.Emoji.reload()
899 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
900 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
902 User.toggle_confirmation(users)
904 ModerationLog.insert_log(%{
907 action: "confirm_email"
913 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
914 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
916 User.try_send_confirmation_email(users)
918 ModerationLog.insert_log(%{
921 action: "resend_confirmation_email"
927 def oauth_app_create(conn, params) do
930 Map.put(params, "client_name", params["name"])
936 case App.create(params) do
938 AppView.render("show.json", %{app: app, admin: true})
940 {:error, changeset} ->
941 App.errors(changeset)
947 def oauth_app_update(conn, params) do
950 Map.put(params, "client_name", params["name"])
955 with {:ok, app} <- App.update(params) do
956 json(conn, AppView.render("show.json", %{app: app, admin: true}))
958 {:error, changeset} ->
959 json(conn, App.errors(changeset))
962 json_response(conn, :bad_request, "")
966 def oauth_app_list(conn, params) do
967 {page, page_size} = page_params(params)
970 client_name: params["name"],
971 client_id: params["client_id"],
977 if Map.has_key?(params, "trusted") do
978 Map.put(search_params, :trusted, params["trusted"])
983 with {:ok, apps, count} <- App.search(search_params) do
986 AppView.render("index.json",
989 page_size: page_size,
996 def oauth_app_delete(conn, params) do
997 with {:ok, _app} <- App.destroy(params["id"]) do
998 json_response(conn, :no_content, "")
1000 _ -> json_response(conn, :bad_request, "")
1004 def stats(conn, _) do
1005 count = Stats.get_status_visibility_count()
1008 |> json(%{"status_visibility" => count})
1011 defp page_params(params) do
1012 {get_page(params["page"]), get_page_size(params["page_size"])}
1015 defp get_page(page_string) when is_nil(page_string), do: 1
1017 defp get_page(page_string) do
1018 case Integer.parse(page_string) do
1024 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1026 defp get_page_size(page_size_string) do
1027 case Integer.parse(page_size_string) do
1028 {page_size, _} -> page_size
1029 :error -> @users_page_size