DELETE /api/pleroma/admin/users now accepts nicknames array
[akkoma] / lib / pleroma / web / admin_api / admin_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.AdminAPI.AdminAPIController do
6 use Pleroma.Web, :controller
7 alias Pleroma.Activity
8 alias Pleroma.ModerationLog
9 alias Pleroma.Plugs.OAuthScopesPlug
10 alias Pleroma.User
11 alias Pleroma.UserInviteToken
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Relay
14 alias Pleroma.Web.AdminAPI.AccountView
15 alias Pleroma.Web.AdminAPI.Config
16 alias Pleroma.Web.AdminAPI.ConfigView
17 alias Pleroma.Web.AdminAPI.ModerationLogView
18 alias Pleroma.Web.AdminAPI.Report
19 alias Pleroma.Web.AdminAPI.ReportView
20 alias Pleroma.Web.AdminAPI.Search
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Web.MastodonAPI.StatusView
24 alias Pleroma.Web.Router
25
26 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
27
28 require Logger
29
30 plug(
31 OAuthScopesPlug,
32 %{scopes: ["read:accounts"]}
33 when action in [:list_users, :user_show, :right_get, :invites]
34 )
35
36 plug(
37 OAuthScopesPlug,
38 %{scopes: ["write:accounts"]}
39 when action in [
40 :get_invite_token,
41 :revoke_invite,
42 :email_invite,
43 :get_password_reset,
44 :user_follow,
45 :user_unfollow,
46 :user_delete,
47 :users_create,
48 :user_toggle_activation,
49 :user_activate,
50 :user_deactivate,
51 :tag_users,
52 :untag_users,
53 :right_add,
54 :right_delete,
55 :set_activation_status
56 ]
57 )
58
59 plug(
60 OAuthScopesPlug,
61 %{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
62 )
63
64 plug(
65 OAuthScopesPlug,
66 %{scopes: ["write:reports"]}
67 when action in [:report_update_state, :report_respond]
68 )
69
70 plug(
71 OAuthScopesPlug,
72 %{scopes: ["read:statuses"]} when action == :list_user_statuses
73 )
74
75 plug(
76 OAuthScopesPlug,
77 %{scopes: ["write:statuses"]}
78 when action in [:status_update, :status_delete]
79 )
80
81 plug(
82 OAuthScopesPlug,
83 %{scopes: ["read"]}
84 when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
85 )
86
87 plug(
88 OAuthScopesPlug,
89 %{scopes: ["write"]}
90 when action in [:relay_follow, :relay_unfollow, :config_update]
91 )
92
93 @users_page_size 50
94
95 action_fallback(:errors)
96
97 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
98 user = User.get_cached_by_nickname(nickname)
99 User.delete(user)
100
101 ModerationLog.insert_log(%{
102 actor: admin,
103 subject: [user],
104 action: "delete"
105 })
106
107 conn
108 |> json(nickname)
109 end
110
111 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
112 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
113 User.delete(users)
114
115 ModerationLog.insert_log(%{
116 actor: admin,
117 subject: users,
118 action: "delete"
119 })
120
121 conn
122 |> json(nicknames)
123 end
124
125 def user_follow(%{assigns: %{user: admin}} = conn, %{
126 "follower" => follower_nick,
127 "followed" => followed_nick
128 }) do
129 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
130 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
131 User.follow(follower, followed)
132
133 ModerationLog.insert_log(%{
134 actor: admin,
135 followed: followed,
136 follower: follower,
137 action: "follow"
138 })
139 end
140
141 conn
142 |> json("ok")
143 end
144
145 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
146 "follower" => follower_nick,
147 "followed" => followed_nick
148 }) do
149 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
150 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
151 User.unfollow(follower, followed)
152
153 ModerationLog.insert_log(%{
154 actor: admin,
155 followed: followed,
156 follower: follower,
157 action: "unfollow"
158 })
159 end
160
161 conn
162 |> json("ok")
163 end
164
165 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
166 changesets =
167 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
168 user_data = %{
169 nickname: nickname,
170 name: nickname,
171 email: email,
172 password: password,
173 password_confirmation: password,
174 bio: "."
175 }
176
177 User.register_changeset(%User{}, user_data, need_confirmation: false)
178 end)
179 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
180 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
181 end)
182
183 case Pleroma.Repo.transaction(changesets) do
184 {:ok, users} ->
185 res =
186 users
187 |> Map.values()
188 |> Enum.map(fn user ->
189 {:ok, user} = User.post_register_action(user)
190
191 user
192 end)
193 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
194
195 ModerationLog.insert_log(%{
196 actor: admin,
197 subjects: Map.values(users),
198 action: "create"
199 })
200
201 conn
202 |> json(res)
203
204 {:error, id, changeset, _} ->
205 res =
206 Enum.map(changesets.operations, fn
207 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
208 AccountView.render("create-error.json", %{changeset: changeset})
209
210 {_, {:changeset, current_changeset, _}} ->
211 AccountView.render("create-error.json", %{changeset: current_changeset})
212 end)
213
214 conn
215 |> put_status(:conflict)
216 |> json(res)
217 end
218 end
219
220 def user_show(conn, %{"nickname" => nickname}) do
221 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
222 conn
223 |> put_view(AccountView)
224 |> render("show.json", %{user: user})
225 else
226 _ -> {:error, :not_found}
227 end
228 end
229
230 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
231 godmode = params["godmode"] == "true" || params["godmode"] == true
232
233 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
234 {_, page_size} = page_params(params)
235
236 activities =
237 ActivityPub.fetch_user_activities(user, nil, %{
238 "limit" => page_size,
239 "godmode" => godmode
240 })
241
242 conn
243 |> put_view(StatusView)
244 |> render("index.json", %{activities: activities, as: :activity})
245 else
246 _ -> {:error, :not_found}
247 end
248 end
249
250 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
251 user = User.get_cached_by_nickname(nickname)
252
253 {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
254
255 action = if user.info.deactivated, do: "activate", else: "deactivate"
256
257 ModerationLog.insert_log(%{
258 actor: admin,
259 subject: [user],
260 action: action
261 })
262
263 conn
264 |> put_view(AccountView)
265 |> render("show.json", %{user: updated_user})
266 end
267
268 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
269 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
270 {:ok, updated_users} = User.deactivate(users, false)
271
272 ModerationLog.insert_log(%{
273 actor: admin,
274 subject: users,
275 action: "activate"
276 })
277
278 conn
279 |> put_view(AccountView)
280 |> render("index.json", %{users: Keyword.values(updated_users)})
281 end
282
283 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
284 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
285 {:ok, updated_users} = User.deactivate(users, true)
286
287 ModerationLog.insert_log(%{
288 actor: admin,
289 subject: users,
290 action: "deactivate"
291 })
292
293 conn
294 |> put_view(AccountView)
295 |> render("index.json", %{users: Keyword.values(updated_users)})
296 end
297
298 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
299 with {:ok, _} <- User.tag(nicknames, tags) do
300 ModerationLog.insert_log(%{
301 actor: admin,
302 nicknames: nicknames,
303 tags: tags,
304 action: "tag"
305 })
306
307 json_response(conn, :no_content, "")
308 end
309 end
310
311 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
312 with {:ok, _} <- User.untag(nicknames, tags) do
313 ModerationLog.insert_log(%{
314 actor: admin,
315 nicknames: nicknames,
316 tags: tags,
317 action: "untag"
318 })
319
320 json_response(conn, :no_content, "")
321 end
322 end
323
324 def list_users(conn, params) do
325 {page, page_size} = page_params(params)
326 filters = maybe_parse_filters(params["filters"])
327
328 search_params = %{
329 query: params["query"],
330 page: page,
331 page_size: page_size,
332 tags: params["tags"],
333 name: params["name"],
334 email: params["email"]
335 }
336
337 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
338 do:
339 conn
340 |> json(
341 AccountView.render("index.json",
342 users: users,
343 count: count,
344 page_size: page_size
345 )
346 )
347 end
348
349 @filters ~w(local external active deactivated is_admin is_moderator)
350
351 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
352 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
353
354 defp maybe_parse_filters(filters) do
355 filters
356 |> String.split(",")
357 |> Enum.filter(&Enum.member?(@filters, &1))
358 |> Enum.map(&String.to_atom(&1))
359 |> Enum.into(%{}, &{&1, true})
360 end
361
362 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
363 "permission_group" => permission_group,
364 "nicknames" => nicknames
365 })
366 when permission_group in ["moderator", "admin"] do
367 info = Map.put(%{}, "is_" <> permission_group, true)
368
369 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
370
371 User.update_info(users, &User.Info.admin_api_update(&1, info))
372
373 ModerationLog.insert_log(%{
374 action: "grant",
375 actor: admin,
376 subject: users,
377 permission: permission_group
378 })
379
380 json(conn, info)
381 end
382
383 def right_add_multiple(conn, _) do
384 render_error(conn, :not_found, "No such permission_group")
385 end
386
387 def right_add(%{assigns: %{user: admin}} = conn, %{
388 "permission_group" => permission_group,
389 "nickname" => nickname
390 })
391 when permission_group in ["moderator", "admin"] do
392 info = Map.put(%{}, "is_" <> permission_group, true)
393
394 {:ok, user} =
395 nickname
396 |> User.get_cached_by_nickname()
397 |> User.update_info(&User.Info.admin_api_update(&1, info))
398
399 ModerationLog.insert_log(%{
400 action: "grant",
401 actor: admin,
402 subject: [user],
403 permission: permission_group
404 })
405
406 json(conn, info)
407 end
408
409 def right_add(conn, _) do
410 render_error(conn, :not_found, "No such permission_group")
411 end
412
413 def right_get(conn, %{"nickname" => nickname}) do
414 user = User.get_cached_by_nickname(nickname)
415
416 conn
417 |> json(%{
418 is_moderator: user.info.is_moderator,
419 is_admin: user.info.is_admin
420 })
421 end
422
423 def right_delete_multiple(
424 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
425 %{
426 "permission_group" => permission_group,
427 "nicknames" => nicknames
428 }
429 )
430 when permission_group in ["moderator", "admin"] do
431 with false <- Enum.member?(nicknames, admin_nickname) do
432 info = Map.put(%{}, "is_" <> permission_group, false)
433
434 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
435
436 User.update_info(users, &User.Info.admin_api_update(&1, info))
437
438 ModerationLog.insert_log(%{
439 action: "revoke",
440 actor: admin,
441 subject: users,
442 permission: permission_group
443 })
444
445 json(conn, info)
446 else
447 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
448 end
449 end
450
451 def right_delete_multiple(conn, _) do
452 render_error(conn, :not_found, "No such permission_group")
453 end
454
455 def right_delete(
456 %{assigns: %{user: admin}} = conn,
457 %{
458 "permission_group" => permission_group,
459 "nickname" => nickname
460 }
461 )
462 when permission_group in ["moderator", "admin"] do
463 info = Map.put(%{}, "is_" <> permission_group, false)
464
465 {:ok, user} =
466 nickname
467 |> User.get_cached_by_nickname()
468 |> User.update_info(&User.Info.admin_api_update(&1, info))
469
470 ModerationLog.insert_log(%{
471 action: "revoke",
472 actor: admin,
473 subject: [user],
474 permission: permission_group
475 })
476
477 json(conn, info)
478 end
479
480 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
481 render_error(conn, :forbidden, "You can't revoke your own admin status.")
482 end
483
484 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
485 with {:ok, _message} <- Relay.follow(target) do
486 ModerationLog.insert_log(%{
487 action: "relay_follow",
488 actor: admin,
489 target: target
490 })
491
492 json(conn, target)
493 else
494 _ ->
495 conn
496 |> put_status(500)
497 |> json(target)
498 end
499 end
500
501 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
502 with {:ok, _message} <- Relay.unfollow(target) do
503 ModerationLog.insert_log(%{
504 action: "relay_unfollow",
505 actor: admin,
506 target: target
507 })
508
509 json(conn, target)
510 else
511 _ ->
512 conn
513 |> put_status(500)
514 |> json(target)
515 end
516 end
517
518 @doc "Sends registration invite via email"
519 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
520 with true <-
521 Pleroma.Config.get([:instance, :invites_enabled]) &&
522 !Pleroma.Config.get([:instance, :registrations_open]),
523 {:ok, invite_token} <- UserInviteToken.create_invite(),
524 email <-
525 Pleroma.Emails.UserEmail.user_invitation_email(
526 user,
527 invite_token,
528 email,
529 params["name"]
530 ),
531 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
532 json_response(conn, :no_content, "")
533 end
534 end
535
536 @doc "Create an account registration invite token"
537 def create_invite_token(conn, params) do
538 opts = %{}
539
540 opts =
541 if params["max_use"],
542 do: Map.put(opts, :max_use, params["max_use"]),
543 else: opts
544
545 opts =
546 if params["expires_at"],
547 do: Map.put(opts, :expires_at, params["expires_at"]),
548 else: opts
549
550 {:ok, invite} = UserInviteToken.create_invite(opts)
551
552 json(conn, AccountView.render("invite.json", %{invite: invite}))
553 end
554
555 @doc "Get list of created invites"
556 def invites(conn, _params) do
557 invites = UserInviteToken.list_invites()
558
559 conn
560 |> put_view(AccountView)
561 |> render("invites.json", %{invites: invites})
562 end
563
564 @doc "Revokes invite by token"
565 def revoke_invite(conn, %{"token" => token}) do
566 with {:ok, invite} <- UserInviteToken.find_by_token(token),
567 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
568 conn
569 |> put_view(AccountView)
570 |> render("invite.json", %{invite: updated_invite})
571 else
572 nil -> {:error, :not_found}
573 end
574 end
575
576 @doc "Get a password reset token (base64 string) for given nickname"
577 def get_password_reset(conn, %{"nickname" => nickname}) do
578 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
579 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
580
581 conn
582 |> json(%{
583 token: token.token,
584 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
585 })
586 end
587
588 @doc "Force password reset for a given user"
589 def force_password_reset(conn, %{"nickname" => nickname}) do
590 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
591
592 User.force_password_reset_async(user)
593
594 json_response(conn, :no_content, "")
595 end
596
597 def list_reports(conn, params) do
598 {page, page_size} = page_params(params)
599
600 params =
601 params
602 |> Map.put("type", "Flag")
603 |> Map.put("skip_preload", true)
604 |> Map.put("total", true)
605 |> Map.put("limit", page_size)
606 |> Map.put("offset", (page - 1) * page_size)
607
608 reports = ActivityPub.fetch_activities([], params, :offset)
609
610 conn
611 |> put_view(ReportView)
612 |> render("index.json", %{reports: reports})
613 end
614
615 def report_show(conn, %{"id" => id}) do
616 with %Activity{} = report <- Activity.get_by_id(id) do
617 conn
618 |> put_view(ReportView)
619 |> render("show.json", Report.extract_report_info(report))
620 else
621 _ -> {:error, :not_found}
622 end
623 end
624
625 def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
626 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
627 ModerationLog.insert_log(%{
628 action: "report_update",
629 actor: admin,
630 subject: report
631 })
632
633 conn
634 |> put_view(ReportView)
635 |> render("show.json", Report.extract_report_info(report))
636 end
637 end
638
639 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
640 with false <- is_nil(params["status"]),
641 %Activity{} <- Activity.get_by_id(id) do
642 params =
643 params
644 |> Map.put("in_reply_to_status_id", id)
645 |> Map.put("visibility", "direct")
646
647 {:ok, activity} = CommonAPI.post(user, params)
648
649 ModerationLog.insert_log(%{
650 action: "report_response",
651 actor: user,
652 subject: activity,
653 text: params["status"]
654 })
655
656 conn
657 |> put_view(StatusView)
658 |> render("show.json", %{activity: activity})
659 else
660 true ->
661 {:param_cast, nil}
662
663 nil ->
664 {:error, :not_found}
665 end
666 end
667
668 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
669 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
670 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
671
672 ModerationLog.insert_log(%{
673 action: "status_update",
674 actor: admin,
675 subject: activity,
676 sensitive: sensitive,
677 visibility: params["visibility"]
678 })
679
680 conn
681 |> put_view(StatusView)
682 |> render("show.json", %{activity: activity})
683 end
684 end
685
686 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
687 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
688 ModerationLog.insert_log(%{
689 action: "status_delete",
690 actor: user,
691 subject_id: id
692 })
693
694 json(conn, %{})
695 end
696 end
697
698 def list_log(conn, params) do
699 {page, page_size} = page_params(params)
700
701 log =
702 ModerationLog.get_all(%{
703 page: page,
704 page_size: page_size,
705 start_date: params["start_date"],
706 end_date: params["end_date"],
707 user_id: params["user_id"],
708 search: params["search"]
709 })
710
711 conn
712 |> put_view(ModerationLogView)
713 |> render("index.json", %{log: log})
714 end
715
716 def migrate_to_db(conn, _params) do
717 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
718 json(conn, %{})
719 end
720
721 def migrate_from_db(conn, _params) do
722 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
723 json(conn, %{})
724 end
725
726 def config_show(conn, _params) do
727 configs = Pleroma.Repo.all(Config)
728
729 conn
730 |> put_view(ConfigView)
731 |> render("index.json", %{configs: configs})
732 end
733
734 def config_update(conn, %{"configs" => configs}) do
735 updated =
736 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
737 updated =
738 Enum.map(configs, fn
739 %{"group" => group, "key" => key, "delete" => "true"} = params ->
740 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
741 config
742
743 %{"group" => group, "key" => key, "value" => value} ->
744 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
745 config
746 end)
747 |> Enum.reject(&is_nil(&1))
748
749 Pleroma.Config.TransferTask.load_and_update_env()
750 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
751 updated
752 else
753 []
754 end
755
756 conn
757 |> put_view(ConfigView)
758 |> render("index.json", %{configs: updated})
759 end
760
761 def reload_emoji(conn, _params) do
762 Pleroma.Emoji.reload()
763
764 conn |> json("ok")
765 end
766
767 def errors(conn, {:error, :not_found}) do
768 conn
769 |> put_status(:not_found)
770 |> json(dgettext("errors", "Not found"))
771 end
772
773 def errors(conn, {:error, reason}) do
774 conn
775 |> put_status(:bad_request)
776 |> json(reason)
777 end
778
779 def errors(conn, {:param_cast, _}) do
780 conn
781 |> put_status(:bad_request)
782 |> json(dgettext("errors", "Invalid parameters"))
783 end
784
785 def errors(conn, _) do
786 conn
787 |> put_status(:internal_server_error)
788 |> json(dgettext("errors", "Something went wrong"))
789 end
790
791 defp page_params(params) do
792 {get_page(params["page"]), get_page_size(params["page_size"])}
793 end
794
795 defp get_page(page_string) when is_nil(page_string), do: 1
796
797 defp get_page(page_string) do
798 case Integer.parse(page_string) do
799 {page, _} -> page
800 :error -> 1
801 end
802 end
803
804 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
805
806 defp get_page_size(page_size_string) do
807 case Integer.parse(page_size_string) do
808 {page_size, _} -> page_size
809 :error -> @users_page_size
810 end
811 end
812 end