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.AdminAPI
21 alias Pleroma.Web.AdminAPI.AccountView
22 alias Pleroma.Web.AdminAPI.ConfigView
23 alias Pleroma.Web.AdminAPI.ModerationLogView
24 alias Pleroma.Web.AdminAPI.Search
25 alias Pleroma.Web.Endpoint
26 alias Pleroma.Web.Router
30 @descriptions Pleroma.Docs.JSON.compile()
35 %{scopes: ["read:accounts"], admin: true}
36 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
41 %{scopes: ["write:accounts"], admin: true}
44 :force_password_reset,
47 :user_toggle_activation,
56 :right_delete_multiple,
57 :update_user_credentials
63 %{scopes: ["write:follows"], admin: true}
64 when action in [:user_follow, :user_unfollow]
69 %{scopes: ["read:statuses"], admin: true}
70 when action in [:list_user_statuses, :list_instance_statuses]
75 %{scopes: ["read"], admin: true}
87 %{scopes: ["write"], admin: true}
91 :resend_confirmation_email,
97 action_fallback(AdminAPI.FallbackController)
99 def user_delete(conn, %{"nickname" => nickname}) do
100 user_delete(conn, %{"nicknames" => [nickname]})
103 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
106 |> Enum.map(&User.get_cached_by_nickname/1)
109 |> Enum.each(fn user ->
110 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
111 Pipeline.common_pipeline(delete_data, local: true)
114 ModerationLog.insert_log(%{
124 def user_follow(%{assigns: %{user: admin}} = conn, %{
125 "follower" => follower_nick,
126 "followed" => followed_nick
128 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
129 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
130 User.follow(follower, followed)
132 ModerationLog.insert_log(%{
144 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
145 "follower" => follower_nick,
146 "followed" => followed_nick
148 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
149 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
150 User.unfollow(follower, followed)
152 ModerationLog.insert_log(%{
164 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
166 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
172 password_confirmation: password,
176 User.register_changeset(%User{}, user_data, need_confirmation: false)
178 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
179 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
182 case Pleroma.Repo.transaction(changesets) do
187 |> Enum.map(fn user ->
188 {:ok, user} = User.post_register_action(user)
192 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
194 ModerationLog.insert_log(%{
196 subjects: Map.values(users),
203 {:error, id, changeset, _} ->
205 Enum.map(changesets.operations, fn
206 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
207 AccountView.render("create-error.json", %{changeset: changeset})
209 {_, {:changeset, current_changeset, _}} ->
210 AccountView.render("create-error.json", %{changeset: current_changeset})
214 |> put_status(:conflict)
219 def user_show(conn, %{"nickname" => nickname}) do
220 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
222 |> put_view(AccountView)
223 |> render("show.json", %{user: user})
225 _ -> {:error, :not_found}
229 def list_instance_statuses(conn, %{"instance" => instance} = params) do
230 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
231 {page, page_size} = page_params(params)
234 ActivityPub.fetch_statuses(nil, %{
235 "instance" => instance,
236 "limit" => page_size,
237 "offset" => (page - 1) * page_size,
238 "exclude_reblogs" => !with_reblogs && "true"
242 |> put_view(AdminAPI.StatusView)
243 |> render("index.json", %{activities: activities, as: :activity})
246 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
247 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
248 godmode = params["godmode"] == "true" || params["godmode"] == true
250 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
251 {_, page_size} = page_params(params)
254 ActivityPub.fetch_user_activities(user, nil, %{
255 "limit" => page_size,
256 "godmode" => godmode,
257 "exclude_reblogs" => !with_reblogs && "true"
261 |> put_view(AdminAPI.StatusView)
262 |> render("index.json", %{activities: activities, as: :activity})
264 _ -> {:error, :not_found}
268 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
269 user = User.get_cached_by_nickname(nickname)
271 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
273 action = if user.deactivated, do: "activate", else: "deactivate"
275 ModerationLog.insert_log(%{
282 |> put_view(AccountView)
283 |> render("show.json", %{user: updated_user})
286 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
287 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
288 {:ok, updated_users} = User.deactivate(users, false)
290 ModerationLog.insert_log(%{
297 |> put_view(AccountView)
298 |> render("index.json", %{users: Keyword.values(updated_users)})
301 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
302 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
303 {:ok, updated_users} = User.deactivate(users, true)
305 ModerationLog.insert_log(%{
312 |> put_view(AccountView)
313 |> render("index.json", %{users: Keyword.values(updated_users)})
316 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
317 with {:ok, _} <- User.tag(nicknames, tags) do
318 ModerationLog.insert_log(%{
320 nicknames: nicknames,
325 json_response(conn, :no_content, "")
329 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
330 with {:ok, _} <- User.untag(nicknames, tags) do
331 ModerationLog.insert_log(%{
333 nicknames: nicknames,
338 json_response(conn, :no_content, "")
342 def list_users(conn, params) do
343 {page, page_size} = page_params(params)
344 filters = maybe_parse_filters(params["filters"])
347 query: params["query"],
349 page_size: page_size,
350 tags: params["tags"],
351 name: params["name"],
352 email: params["email"]
355 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
358 AccountView.render("index.json", users: users, count: count, page_size: page_size)
363 @filters ~w(local external active deactivated is_admin is_moderator)
365 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
366 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
368 defp maybe_parse_filters(filters) do
371 |> Enum.filter(&Enum.member?(@filters, &1))
372 |> Enum.map(&String.to_atom(&1))
373 |> Enum.into(%{}, &{&1, true})
376 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
377 "permission_group" => permission_group,
378 "nicknames" => nicknames
380 when permission_group in ["moderator", "admin"] do
381 update = %{:"is_#{permission_group}" => true}
383 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
385 for u <- users, do: User.admin_api_update(u, update)
387 ModerationLog.insert_log(%{
391 permission: permission_group
397 def right_add_multiple(conn, _) do
398 render_error(conn, :not_found, "No such permission_group")
401 def right_add(%{assigns: %{user: admin}} = conn, %{
402 "permission_group" => permission_group,
403 "nickname" => nickname
405 when permission_group in ["moderator", "admin"] do
406 fields = %{:"is_#{permission_group}" => true}
410 |> User.get_cached_by_nickname()
411 |> User.admin_api_update(fields)
413 ModerationLog.insert_log(%{
417 permission: permission_group
423 def right_add(conn, _) do
424 render_error(conn, :not_found, "No such permission_group")
427 def right_get(conn, %{"nickname" => nickname}) do
428 user = User.get_cached_by_nickname(nickname)
432 is_moderator: user.is_moderator,
433 is_admin: user.is_admin
437 def right_delete_multiple(
438 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
440 "permission_group" => permission_group,
441 "nicknames" => nicknames
444 when permission_group in ["moderator", "admin"] do
445 with false <- Enum.member?(nicknames, admin_nickname) do
446 update = %{:"is_#{permission_group}" => false}
448 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
450 for u <- users, do: User.admin_api_update(u, update)
452 ModerationLog.insert_log(%{
456 permission: permission_group
461 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
465 def right_delete_multiple(conn, _) do
466 render_error(conn, :not_found, "No such permission_group")
470 %{assigns: %{user: admin}} = conn,
472 "permission_group" => permission_group,
473 "nickname" => nickname
476 when permission_group in ["moderator", "admin"] do
477 fields = %{:"is_#{permission_group}" => false}
481 |> User.get_cached_by_nickname()
482 |> User.admin_api_update(fields)
484 ModerationLog.insert_log(%{
488 permission: permission_group
494 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
495 render_error(conn, :forbidden, "You can't revoke your own admin status.")
498 @doc "Get a password reset token (base64 string) for given nickname"
499 def get_password_reset(conn, %{"nickname" => nickname}) do
500 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
501 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
506 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
510 @doc "Force password reset for a given user"
511 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
512 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
514 Enum.each(users, &User.force_password_reset_async/1)
516 ModerationLog.insert_log(%{
519 action: "force_password_reset"
522 json_response(conn, :no_content, "")
525 @doc "Disable mfa for user's account."
526 def disable_mfa(conn, %{"nickname" => nickname}) do
527 case User.get_by_nickname(nickname) do
537 @doc "Show a given user's credentials"
538 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
539 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
541 |> put_view(AccountView)
542 |> render("credentials.json", %{user: user, for: admin})
544 _ -> {:error, :not_found}
548 @doc "Updates a given user"
549 def update_user_credentials(
550 %{assigns: %{user: admin}} = conn,
551 %{"nickname" => nickname} = params
553 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
555 User.update_as_admin(user, params) do
556 ModerationLog.insert_log(%{
559 action: "updated_users"
562 if params["password"] do
563 User.force_password_reset_async(user)
566 ModerationLog.insert_log(%{
569 action: "force_password_reset"
572 json(conn, %{status: "success"})
574 {:error, changeset} ->
575 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
577 json(conn, %{errors: errors})
580 json(conn, %{error: "Unable to update user."})
584 def list_log(conn, params) do
585 {page, page_size} = page_params(params)
588 ModerationLog.get_all(%{
590 page_size: page_size,
591 start_date: params["start_date"],
592 end_date: params["end_date"],
593 user_id: params["user_id"],
594 search: params["search"]
598 |> put_view(ModerationLogView)
599 |> render("index.json", %{log: log})
602 def config_descriptions(conn, _params) do
603 descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
605 json(conn, descriptions)
608 def config_show(conn, %{"only_db" => true}) do
609 with :ok <- configurable_from_database() do
610 configs = Pleroma.Repo.all(ConfigDB)
613 |> put_view(ConfigView)
614 |> render("index.json", %{configs: configs})
618 def config_show(conn, _params) do
619 with :ok <- configurable_from_database() do
620 configs = ConfigDB.get_all_as_keyword()
623 Config.Holder.default_config()
624 |> ConfigDB.merge(configs)
625 |> Enum.map(fn {group, values} ->
626 Enum.map(values, fn {key, value} ->
628 if configs[group][key] do
629 ConfigDB.get_db_keys(configs[group][key], key)
632 db_value = configs[group][key]
635 if !is_nil(db_value) and Keyword.keyword?(db_value) and
636 ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
637 ConfigDB.merge_group(group, key, value, db_value)
643 group: ConfigDB.convert(group),
644 key: ConfigDB.convert(key),
645 value: ConfigDB.convert(merged_value)
648 if db, do: Map.put(setting, :db, db), else: setting
653 json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
657 def config_update(conn, %{"configs" => configs}) do
658 with :ok <- configurable_from_database() do
661 |> Enum.filter(&whitelisted_config?/1)
663 %{"group" => group, "key" => key, "delete" => true} = params ->
664 ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
666 %{"group" => group, "key" => key, "value" => value} ->
667 ConfigDB.update_or_create(%{group: group, key: key, value: value})
669 |> Enum.split_with(fn result -> elem(result, 0) == :error end)
673 |> Enum.map(fn {:ok, config} ->
674 Map.put(config, :db, ConfigDB.get_db_keys(config))
676 |> Enum.split_with(fn config ->
677 Ecto.get_meta(config, :state) == :deleted
680 Config.TransferTask.load_and_update_env(deleted, false)
682 if !Restarter.Pleroma.need_reboot?() do
683 changed_reboot_settings? =
685 |> Enum.any?(fn config ->
686 group = ConfigDB.from_string(config.group)
687 key = ConfigDB.from_string(config.key)
688 value = ConfigDB.from_binary(config.value)
689 Config.TransferTask.pleroma_need_restart?(group, key, value)
692 if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
696 |> put_view(ConfigView)
697 |> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
701 def restart(conn, _params) do
702 with :ok <- configurable_from_database() do
703 Restarter.Pleroma.restart(Config.get(:env), 50)
709 def need_reboot(conn, _params) do
710 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
713 defp configurable_from_database do
714 if Config.get(:configurable_from_database) do
717 {:error, "To use this endpoint you need to enable configuration from database."}
721 defp whitelisted_config?(group, key) do
722 if whitelisted_configs = Config.get(:database_config_whitelist) do
723 Enum.any?(whitelisted_configs, fn
724 {whitelisted_group} ->
725 group == inspect(whitelisted_group)
727 {whitelisted_group, whitelisted_key} ->
728 group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
735 defp whitelisted_config?(%{"group" => group, "key" => key}) do
736 whitelisted_config?(group, key)
739 defp whitelisted_config?(%{:group => group} = config) do
740 whitelisted_config?(group, config[:key])
743 def reload_emoji(conn, _params) do
744 Pleroma.Emoji.reload()
749 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
750 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
752 User.toggle_confirmation(users)
754 ModerationLog.insert_log(%{
757 action: "confirm_email"
763 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
764 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
766 User.try_send_confirmation_email(users)
768 ModerationLog.insert_log(%{
771 action: "resend_confirmation_email"
777 def stats(conn, _) do
778 count = Stats.get_status_visibility_count()
781 |> json(%{"status_visibility" => count})
784 defp page_params(params) do
785 {get_page(params["page"]), get_page_size(params["page_size"])}
788 defp get_page(page_string) when is_nil(page_string), do: 1
790 defp get_page(page_string) do
791 case Integer.parse(page_string) do
797 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
799 defp get_page_size(page_size_string) do
800 case Integer.parse(page_size_string) do
801 {page_size, _} -> page_size
802 :error -> @users_page_size