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.UserInviteToken
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Builder
22 alias Pleroma.Web.ActivityPub.Pipeline
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
71 plug(OAuthScopesPlug, %{scopes: ["read:invites"], admin: true} when action == :invites)
75 %{scopes: ["write:invites"], admin: true}
76 when action in [:create_invite_token, :revoke_invite, :email_invite]
81 %{scopes: ["write:follows"], admin: true}
82 when action in [:user_follow, :user_unfollow]
87 %{scopes: ["read:reports"], admin: true}
88 when action in [:list_reports, :report_show]
93 %{scopes: ["write:reports"], admin: true}
94 when action in [:reports_update, :report_notes_create, :report_notes_delete]
99 %{scopes: ["read:statuses"], admin: true}
100 when action in [:list_user_statuses, :list_instance_statuses]
105 %{scopes: ["read"], admin: true}
110 :config_descriptions,
117 %{scopes: ["write"], admin: true}
121 :resend_confirmation_email,
131 action_fallback(AdminAPI.FallbackController)
133 def user_delete(conn, %{"nickname" => nickname}) do
134 user_delete(conn, %{"nicknames" => [nickname]})
137 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
140 |> Enum.map(&User.get_cached_by_nickname/1)
143 |> Enum.each(fn user ->
144 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
145 Pipeline.common_pipeline(delete_data, local: true)
148 ModerationLog.insert_log(%{
158 def user_follow(%{assigns: %{user: admin}} = conn, %{
159 "follower" => follower_nick,
160 "followed" => followed_nick
162 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
163 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
164 User.follow(follower, followed)
166 ModerationLog.insert_log(%{
178 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
179 "follower" => follower_nick,
180 "followed" => followed_nick
182 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
183 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
184 User.unfollow(follower, followed)
186 ModerationLog.insert_log(%{
198 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
200 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
206 password_confirmation: password,
210 User.register_changeset(%User{}, user_data, need_confirmation: false)
212 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
213 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
216 case Pleroma.Repo.transaction(changesets) do
221 |> Enum.map(fn user ->
222 {:ok, user} = User.post_register_action(user)
226 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
228 ModerationLog.insert_log(%{
230 subjects: Map.values(users),
237 {:error, id, changeset, _} ->
239 Enum.map(changesets.operations, fn
240 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
241 AccountView.render("create-error.json", %{changeset: changeset})
243 {_, {:changeset, current_changeset, _}} ->
244 AccountView.render("create-error.json", %{changeset: current_changeset})
248 |> put_status(:conflict)
253 def user_show(conn, %{"nickname" => nickname}) do
254 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
256 |> put_view(AccountView)
257 |> render("show.json", %{user: user})
259 _ -> {:error, :not_found}
263 def list_instance_statuses(conn, %{"instance" => instance} = params) do
264 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
265 {page, page_size} = page_params(params)
268 ActivityPub.fetch_statuses(nil, %{
269 "instance" => instance,
270 "limit" => page_size,
271 "offset" => (page - 1) * page_size,
272 "exclude_reblogs" => !with_reblogs && "true"
276 |> put_view(AdminAPI.StatusView)
277 |> render("index.json", %{activities: activities, as: :activity})
280 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
281 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
282 godmode = params["godmode"] == "true" || params["godmode"] == true
284 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
285 {_, page_size} = page_params(params)
288 ActivityPub.fetch_user_activities(user, nil, %{
289 "limit" => page_size,
290 "godmode" => godmode,
291 "exclude_reblogs" => !with_reblogs && "true"
295 |> put_view(MastodonAPI.StatusView)
296 |> render("index.json", %{activities: activities, as: :activity})
298 _ -> {:error, :not_found}
302 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
303 user = User.get_cached_by_nickname(nickname)
305 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
307 action = if user.deactivated, do: "activate", else: "deactivate"
309 ModerationLog.insert_log(%{
316 |> put_view(AccountView)
317 |> render("show.json", %{user: updated_user})
320 def user_activate(%{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, false)
324 ModerationLog.insert_log(%{
331 |> put_view(AccountView)
332 |> render("index.json", %{users: Keyword.values(updated_users)})
335 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
336 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
337 {:ok, updated_users} = User.deactivate(users, true)
339 ModerationLog.insert_log(%{
346 |> put_view(AccountView)
347 |> render("index.json", %{users: Keyword.values(updated_users)})
350 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
351 with {:ok, _} <- User.tag(nicknames, tags) do
352 ModerationLog.insert_log(%{
354 nicknames: nicknames,
359 json_response(conn, :no_content, "")
363 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
364 with {:ok, _} <- User.untag(nicknames, tags) do
365 ModerationLog.insert_log(%{
367 nicknames: nicknames,
372 json_response(conn, :no_content, "")
376 def list_users(conn, params) do
377 {page, page_size} = page_params(params)
378 filters = maybe_parse_filters(params["filters"])
381 query: params["query"],
383 page_size: page_size,
384 tags: params["tags"],
385 name: params["name"],
386 email: params["email"]
389 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
392 AccountView.render("index.json", users: users, count: count, page_size: page_size)
397 @filters ~w(local external active deactivated is_admin is_moderator)
399 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
400 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
402 defp maybe_parse_filters(filters) do
405 |> Enum.filter(&Enum.member?(@filters, &1))
406 |> Enum.map(&String.to_atom(&1))
407 |> Enum.into(%{}, &{&1, true})
410 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
411 "permission_group" => permission_group,
412 "nicknames" => nicknames
414 when permission_group in ["moderator", "admin"] do
415 update = %{:"is_#{permission_group}" => true}
417 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
419 for u <- users, do: User.admin_api_update(u, update)
421 ModerationLog.insert_log(%{
425 permission: permission_group
431 def right_add_multiple(conn, _) do
432 render_error(conn, :not_found, "No such permission_group")
435 def right_add(%{assigns: %{user: admin}} = conn, %{
436 "permission_group" => permission_group,
437 "nickname" => nickname
439 when permission_group in ["moderator", "admin"] do
440 fields = %{:"is_#{permission_group}" => true}
444 |> User.get_cached_by_nickname()
445 |> User.admin_api_update(fields)
447 ModerationLog.insert_log(%{
451 permission: permission_group
457 def right_add(conn, _) do
458 render_error(conn, :not_found, "No such permission_group")
461 def right_get(conn, %{"nickname" => nickname}) do
462 user = User.get_cached_by_nickname(nickname)
466 is_moderator: user.is_moderator,
467 is_admin: user.is_admin
471 def right_delete_multiple(
472 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
474 "permission_group" => permission_group,
475 "nicknames" => nicknames
478 when permission_group in ["moderator", "admin"] do
479 with false <- Enum.member?(nicknames, admin_nickname) do
480 update = %{:"is_#{permission_group}" => false}
482 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
484 for u <- users, do: User.admin_api_update(u, update)
486 ModerationLog.insert_log(%{
490 permission: permission_group
495 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
499 def right_delete_multiple(conn, _) do
500 render_error(conn, :not_found, "No such permission_group")
504 %{assigns: %{user: admin}} = conn,
506 "permission_group" => permission_group,
507 "nickname" => nickname
510 when permission_group in ["moderator", "admin"] do
511 fields = %{:"is_#{permission_group}" => false}
515 |> User.get_cached_by_nickname()
516 |> User.admin_api_update(fields)
518 ModerationLog.insert_log(%{
522 permission: permission_group
528 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
529 render_error(conn, :forbidden, "You can't revoke your own admin status.")
532 @doc "Sends registration invite via email"
533 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
534 with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
535 {_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
536 {:ok, invite_token} <- UserInviteToken.create_invite(),
538 Pleroma.Emails.UserEmail.user_invitation_email(
544 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
545 json_response(conn, :no_content, "")
547 {:registrations_open, _} ->
548 {:error, "To send invites you need to set the `registrations_open` option to false."}
550 {:invites_enabled, _} ->
551 {:error, "To send invites you need to set the `invites_enabled` option to true."}
555 @doc "Create an account registration invite token"
556 def create_invite_token(conn, params) do
560 if params["max_use"],
561 do: Map.put(opts, :max_use, params["max_use"]),
565 if params["expires_at"],
566 do: Map.put(opts, :expires_at, params["expires_at"]),
569 {:ok, invite} = UserInviteToken.create_invite(opts)
571 json(conn, AccountView.render("invite.json", %{invite: invite}))
574 @doc "Get list of created invites"
575 def invites(conn, _params) do
576 invites = UserInviteToken.list_invites()
579 |> put_view(AccountView)
580 |> render("invites.json", %{invites: invites})
583 @doc "Revokes invite by token"
584 def revoke_invite(conn, %{"token" => token}) do
585 with {:ok, invite} <- UserInviteToken.find_by_token(token),
586 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
588 |> put_view(AccountView)
589 |> render("invite.json", %{invite: updated_invite})
591 nil -> {:error, :not_found}
595 @doc "Get a password reset token (base64 string) for given nickname"
596 def get_password_reset(conn, %{"nickname" => nickname}) do
597 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
598 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
603 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
607 @doc "Force password reset for a given user"
608 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
609 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
611 Enum.each(users, &User.force_password_reset_async/1)
613 ModerationLog.insert_log(%{
616 action: "force_password_reset"
619 json_response(conn, :no_content, "")
622 @doc "Disable mfa for user's account."
623 def disable_mfa(conn, %{"nickname" => nickname}) do
624 case User.get_by_nickname(nickname) do
634 @doc "Show a given user's credentials"
635 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
636 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
638 |> put_view(AccountView)
639 |> render("credentials.json", %{user: user, for: admin})
641 _ -> {:error, :not_found}
645 @doc "Updates a given user"
646 def update_user_credentials(
647 %{assigns: %{user: admin}} = conn,
648 %{"nickname" => nickname} = params
650 with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
652 User.update_as_admin(user, params) do
653 ModerationLog.insert_log(%{
656 action: "updated_users"
659 if params["password"] do
660 User.force_password_reset_async(user)
663 ModerationLog.insert_log(%{
666 action: "force_password_reset"
669 json(conn, %{status: "success"})
671 {:error, changeset} ->
672 {_, {error, _}} = Enum.at(changeset.errors, 0)
673 json(conn, %{error: "New password #{error}."})
676 json(conn, %{error: "Unable to change password."})
680 def list_reports(conn, params) do
681 {page, page_size} = page_params(params)
683 reports = Utils.get_reports(params, page, page_size)
686 |> put_view(ReportView)
687 |> render("index.json", %{reports: reports})
690 def report_show(conn, %{"id" => id}) do
691 with %Activity{} = report <- Activity.get_by_id(id) do
693 |> put_view(ReportView)
694 |> render("show.json", Report.extract_report_info(report))
696 _ -> {:error, :not_found}
700 def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) do
703 |> Enum.map(fn report ->
704 with {:ok, activity} <- CommonAPI.update_report_state(report["id"], report["state"]) do
705 ModerationLog.insert_log(%{
706 action: "report_update",
713 {:error, message} -> %{id: report["id"], error: message}
717 case Enum.any?(result, &Map.has_key?(&1, :error)) do
718 true -> json_response(conn, :bad_request, result)
719 false -> json_response(conn, :no_content, "")
723 def report_notes_create(%{assigns: %{user: user}} = conn, %{
727 with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
728 ModerationLog.insert_log(%{
729 action: "report_note",
731 subject: Activity.get_by_id(report_id),
735 json_response(conn, :no_content, "")
737 _ -> json_response(conn, :bad_request, "")
741 def report_notes_delete(%{assigns: %{user: user}} = conn, %{
743 "report_id" => report_id
745 with {:ok, note} <- ReportNote.destroy(note_id) do
746 ModerationLog.insert_log(%{
747 action: "report_note_delete",
749 subject: Activity.get_by_id(report_id),
753 json_response(conn, :no_content, "")
755 _ -> json_response(conn, :bad_request, "")
759 def list_log(conn, params) do
760 {page, page_size} = page_params(params)
763 ModerationLog.get_all(%{
765 page_size: page_size,
766 start_date: params["start_date"],
767 end_date: params["end_date"],
768 user_id: params["user_id"],
769 search: params["search"]
773 |> put_view(ModerationLogView)
774 |> render("index.json", %{log: log})
777 def config_descriptions(conn, _params) do
778 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
780 json(conn, descriptions)
783 def config_show(conn, %{"only_db" => true}) do
784 with :ok <- configurable_from_database() do
785 configs = Pleroma.Repo.all(ConfigDB)
788 |> put_view(ConfigView)
789 |> render("index.json", %{configs: configs})
793 def config_show(conn, _params) do
794 with :ok <- configurable_from_database() do
795 configs = ConfigDB.get_all_as_keyword()
798 Config.Holder.default_config()
799 |> ConfigDB.merge(configs)
800 |> Enum.map(fn {group, values} ->
801 Enum.map(values, fn {key, value} ->
803 if configs[group][key] do
804 ConfigDB.get_db_keys(configs[group][key], key)
807 db_value = configs[group][key]
810 if !is_nil(db_value) and Keyword.keyword?(db_value) and
811 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
812 ConfigDB.merge_group(group, key, value, db_value)
818 group: ConfigDB.convert(group),
819 key: ConfigDB.convert(key),
820 value: ConfigDB.convert(merged_value)
823 if db, do: Map.put(setting, :db, db), else: setting
828 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
832 def config_update(conn, %{"configs" => configs}) do
833 with :ok <- configurable_from_database() do
836 |> Enum.filter(&whitelisted_config?/1)
838 %{"group" => group, "key" => key, "delete" => true} = params ->
839 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
841 %{"group" => group, "key" => key, "value" => value} ->
842 ConfigDB.update_or_create(%{group: group, key: key, value: value})
844 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
848 |> Enum.map(fn {:ok, config} ->
849 Map.put(config, :db, ConfigDB.get_db_keys(config))
851 |> Enum.split_with(fn config ->
852 Ecto.get_meta(config, :state) == :deleted
855 Config.TransferTask.load_and_update_env(deleted, false)
857 if !Restarter.Pleroma.need_reboot?() do
858 changed_reboot_settings? =
860 |> Enum.any?(fn config ->
861 group = ConfigDB.from_string(config.group)
862 key = ConfigDB.from_string(config.key)
863 value = ConfigDB.from_binary(config.value)
864 Config.TransferTask.pleroma_need_restart?(group, key, value)
867 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
871 |> put_view(ConfigView)
872 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
876 def restart(conn, _params) do
877 with :ok <- configurable_from_database() do
878 Restarter.Pleroma.restart(Config.get(:env), 50)
884 def need_reboot(conn, _params) do
885 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
888 defp configurable_from_database do
889 if Config.get(:configurable_from_database) do
892 {:error, "To use this endpoint you need to enable configuration from database."}
896 defp whitelisted_config?(group, key) do
897 if whitelisted_configs = Config.get(:database_config_whitelist) do
898 Enum.any?(whitelisted_configs, fn
899 {whitelisted_group} ->
900 group == inspect(whitelisted_group)
902 {whitelisted_group, whitelisted_key} ->
903 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
910 defp whitelisted_config?(%{"group" => group, "key" => key}) do
911 whitelisted_config?(group, key)
914 defp whitelisted_config?(%{:group => group} = config) do
915 whitelisted_config?(group, config[:key])
918 def reload_emoji(conn, _params) do
919 Pleroma.Emoji.reload()
924 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
925 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
927 User.toggle_confirmation(users)
929 ModerationLog.insert_log(%{
932 action: "confirm_email"
938 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
939 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
941 User.try_send_confirmation_email(users)
943 ModerationLog.insert_log(%{
946 action: "resend_confirmation_email"
952 def oauth_app_create(conn, params) do
955 Map.put(params, "client_name", params["name"])
961 case App.create(params) do
963 AppView.render("show.json", %{app: app, admin: true})
965 {:error, changeset} ->
966 App.errors(changeset)
972 def oauth_app_update(conn, params) do
975 Map.put(params, "client_name", params["name"])
980 with {:ok, app} <- App.update(params) do
981 json(conn, AppView.render("show.json", %{app: app, admin: true}))
983 {:error, changeset} ->
984 json(conn, App.errors(changeset))
987 json_response(conn, :bad_request, "")
991 def oauth_app_list(conn, params) do
992 {page, page_size} = page_params(params)
995 client_name: params["name"],
996 client_id: params["client_id"],
1002 if Map.has_key?(params, "trusted") do
1003 Map.put(search_params, :trusted, params["trusted"])
1008 with {:ok, apps, count} <- App.search(search_params) do
1011 AppView.render("index.json",
1014 page_size: page_size,
1021 def oauth_app_delete(conn, params) do
1022 with {:ok, _app} <- App.destroy(params["id"]) do
1023 json_response(conn, :no_content, "")
1025 _ -> json_response(conn, :bad_request, "")
1029 def stats(conn, _) do
1030 count = Stats.get_status_visibility_count()
1033 |> json(%{"status_visibility" => count})
1036 defp page_params(params) do
1037 {get_page(params["page"]), get_page_size(params["page_size"])}
1040 defp get_page(page_string) when is_nil(page_string), do: 1
1042 defp get_page(page_string) do
1043 case Integer.parse(page_string) do
1049 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
1051 defp get_page_size(page_size_string) do
1052 case Integer.parse(page_size_string) do
1053 {page_size, _} -> page_size
1054 :error -> @users_page_size