Merge remote-tracking branch 'origin/develop' into activity-pub-use-atoms-as-keys
[akkoma] / lib / pleroma / web / admin_api / controllers / admin_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
9
10 alias Pleroma.Config
11 alias Pleroma.MFA
12 alias Pleroma.ModerationLog
13 alias Pleroma.Plugs.OAuthScopesPlug
14 alias Pleroma.Stats
15 alias Pleroma.User
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Builder
18 alias Pleroma.Web.ActivityPub.Pipeline
19 alias Pleroma.Web.ActivityPub.Relay
20 alias Pleroma.Web.AdminAPI
21 alias Pleroma.Web.AdminAPI.AccountView
22 alias Pleroma.Web.AdminAPI.ModerationLogView
23 alias Pleroma.Web.AdminAPI.Search
24 alias Pleroma.Web.Endpoint
25 alias Pleroma.Web.Router
26
27 require Logger
28
29 @users_page_size 50
30
31 plug(
32 OAuthScopesPlug,
33 %{scopes: ["read:accounts"], admin: true}
34 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
35 )
36
37 plug(
38 OAuthScopesPlug,
39 %{scopes: ["write:accounts"], admin: true}
40 when action in [
41 :get_password_reset,
42 :force_password_reset,
43 :user_delete,
44 :users_create,
45 :user_toggle_activation,
46 :user_activate,
47 :user_deactivate,
48 :tag_users,
49 :untag_users,
50 :right_add,
51 :right_add_multiple,
52 :right_delete,
53 :disable_mfa,
54 :right_delete_multiple,
55 :update_user_credentials
56 ]
57 )
58
59 plug(
60 OAuthScopesPlug,
61 %{scopes: ["write:follows"], admin: true}
62 when action in [:user_follow, :user_unfollow, :relay_follow, :relay_unfollow]
63 )
64
65 plug(
66 OAuthScopesPlug,
67 %{scopes: ["read:statuses"], admin: true}
68 when action in [:list_user_statuses, :list_instance_statuses]
69 )
70
71 plug(
72 OAuthScopesPlug,
73 %{scopes: ["read"], admin: true}
74 when action in [
75 :list_log,
76 :stats,
77 :relay_list,
78 :need_reboot
79 ]
80 )
81
82 plug(
83 OAuthScopesPlug,
84 %{scopes: ["write"], admin: true}
85 when action in [
86 :restart,
87 :resend_confirmation_email,
88 :confirm_email,
89 :reload_emoji
90 ]
91 )
92
93 action_fallback(AdminAPI.FallbackController)
94
95 def user_delete(conn, %{"nickname" => nickname}) do
96 user_delete(conn, %{"nicknames" => [nickname]})
97 end
98
99 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
100 users =
101 nicknames
102 |> Enum.map(&User.get_cached_by_nickname/1)
103
104 users
105 |> Enum.each(fn user ->
106 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
107 Pipeline.common_pipeline(delete_data, local: true)
108 end)
109
110 ModerationLog.insert_log(%{
111 actor: admin,
112 subject: users,
113 action: "delete"
114 })
115
116 conn
117 |> json(nicknames)
118 end
119
120 def user_follow(%{assigns: %{user: admin}} = conn, %{
121 "follower" => follower_nick,
122 "followed" => followed_nick
123 }) do
124 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
125 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
126 User.follow(follower, followed)
127
128 ModerationLog.insert_log(%{
129 actor: admin,
130 followed: followed,
131 follower: follower,
132 action: "follow"
133 })
134 end
135
136 conn
137 |> json("ok")
138 end
139
140 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
141 "follower" => follower_nick,
142 "followed" => followed_nick
143 }) do
144 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
145 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
146 User.unfollow(follower, followed)
147
148 ModerationLog.insert_log(%{
149 actor: admin,
150 followed: followed,
151 follower: follower,
152 action: "unfollow"
153 })
154 end
155
156 conn
157 |> json("ok")
158 end
159
160 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
161 changesets =
162 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
163 user_data = %{
164 nickname: nickname,
165 name: nickname,
166 email: email,
167 password: password,
168 password_confirmation: password,
169 bio: "."
170 }
171
172 User.register_changeset(%User{}, user_data, need_confirmation: false)
173 end)
174 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
175 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
176 end)
177
178 case Pleroma.Repo.transaction(changesets) do
179 {:ok, users} ->
180 res =
181 users
182 |> Map.values()
183 |> Enum.map(fn user ->
184 {:ok, user} = User.post_register_action(user)
185
186 user
187 end)
188 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
189
190 ModerationLog.insert_log(%{
191 actor: admin,
192 subjects: Map.values(users),
193 action: "create"
194 })
195
196 conn
197 |> json(res)
198
199 {:error, id, changeset, _} ->
200 res =
201 Enum.map(changesets.operations, fn
202 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
203 AccountView.render("create-error.json", %{changeset: changeset})
204
205 {_, {:changeset, current_changeset, _}} ->
206 AccountView.render("create-error.json", %{changeset: current_changeset})
207 end)
208
209 conn
210 |> put_status(:conflict)
211 |> json(res)
212 end
213 end
214
215 def user_show(conn, %{"nickname" => nickname}) do
216 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
217 conn
218 |> put_view(AccountView)
219 |> render("show.json", %{user: user})
220 else
221 _ -> {:error, :not_found}
222 end
223 end
224
225 def list_instance_statuses(conn, %{"instance" => instance} = params) do
226 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
227 {page, page_size} = page_params(params)
228
229 activities =
230 ActivityPub.fetch_statuses(nil, %{
231 instance: instance,
232 limit: page_size,
233 offset: (page - 1) * page_size,
234 exclude_reblogs: not with_reblogs
235 })
236
237 conn
238 |> put_view(AdminAPI.StatusView)
239 |> render("index.json", %{activities: activities, as: :activity})
240 end
241
242 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
243 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
244 godmode = params["godmode"] == "true" || params["godmode"] == true
245
246 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
247 {_, page_size} = page_params(params)
248
249 activities =
250 ActivityPub.fetch_user_activities(user, nil, %{
251 limit: page_size,
252 godmode: godmode,
253 exclude_reblogs: not with_reblogs
254 })
255
256 conn
257 |> put_view(AdminAPI.StatusView)
258 |> render("index.json", %{activities: activities, as: :activity})
259 else
260 _ -> {:error, :not_found}
261 end
262 end
263
264 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
265 user = User.get_cached_by_nickname(nickname)
266
267 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
268
269 action = if user.deactivated, do: "activate", else: "deactivate"
270
271 ModerationLog.insert_log(%{
272 actor: admin,
273 subject: [user],
274 action: action
275 })
276
277 conn
278 |> put_view(AccountView)
279 |> render("show.json", %{user: updated_user})
280 end
281
282 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
283 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
284 {:ok, updated_users} = User.deactivate(users, false)
285
286 ModerationLog.insert_log(%{
287 actor: admin,
288 subject: users,
289 action: "activate"
290 })
291
292 conn
293 |> put_view(AccountView)
294 |> render("index.json", %{users: Keyword.values(updated_users)})
295 end
296
297 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
298 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
299 {:ok, updated_users} = User.deactivate(users, true)
300
301 ModerationLog.insert_log(%{
302 actor: admin,
303 subject: users,
304 action: "deactivate"
305 })
306
307 conn
308 |> put_view(AccountView)
309 |> render("index.json", %{users: Keyword.values(updated_users)})
310 end
311
312 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
313 with {:ok, _} <- User.tag(nicknames, tags) do
314 ModerationLog.insert_log(%{
315 actor: admin,
316 nicknames: nicknames,
317 tags: tags,
318 action: "tag"
319 })
320
321 json_response(conn, :no_content, "")
322 end
323 end
324
325 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
326 with {:ok, _} <- User.untag(nicknames, tags) do
327 ModerationLog.insert_log(%{
328 actor: admin,
329 nicknames: nicknames,
330 tags: tags,
331 action: "untag"
332 })
333
334 json_response(conn, :no_content, "")
335 end
336 end
337
338 def list_users(conn, params) do
339 {page, page_size} = page_params(params)
340 filters = maybe_parse_filters(params["filters"])
341
342 search_params = %{
343 query: params["query"],
344 page: page,
345 page_size: page_size,
346 tags: params["tags"],
347 name: params["name"],
348 email: params["email"]
349 }
350
351 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
352 json(
353 conn,
354 AccountView.render("index.json", users: users, count: count, page_size: page_size)
355 )
356 end
357 end
358
359 @filters ~w(local external active deactivated is_admin is_moderator)
360
361 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
362 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
363
364 defp maybe_parse_filters(filters) do
365 filters
366 |> String.split(",")
367 |> Enum.filter(&Enum.member?(@filters, &1))
368 |> Enum.map(&String.to_atom(&1))
369 |> Enum.into(%{}, &{&1, true})
370 end
371
372 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
373 "permission_group" => permission_group,
374 "nicknames" => nicknames
375 })
376 when permission_group in ["moderator", "admin"] do
377 update = %{:"is_#{permission_group}" => true}
378
379 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
380
381 for u <- users, do: User.admin_api_update(u, update)
382
383 ModerationLog.insert_log(%{
384 action: "grant",
385 actor: admin,
386 subject: users,
387 permission: permission_group
388 })
389
390 json(conn, update)
391 end
392
393 def right_add_multiple(conn, _) do
394 render_error(conn, :not_found, "No such permission_group")
395 end
396
397 def right_add(%{assigns: %{user: admin}} = conn, %{
398 "permission_group" => permission_group,
399 "nickname" => nickname
400 })
401 when permission_group in ["moderator", "admin"] do
402 fields = %{:"is_#{permission_group}" => true}
403
404 {:ok, user} =
405 nickname
406 |> User.get_cached_by_nickname()
407 |> User.admin_api_update(fields)
408
409 ModerationLog.insert_log(%{
410 action: "grant",
411 actor: admin,
412 subject: [user],
413 permission: permission_group
414 })
415
416 json(conn, fields)
417 end
418
419 def right_add(conn, _) do
420 render_error(conn, :not_found, "No such permission_group")
421 end
422
423 def right_get(conn, %{"nickname" => nickname}) do
424 user = User.get_cached_by_nickname(nickname)
425
426 conn
427 |> json(%{
428 is_moderator: user.is_moderator,
429 is_admin: user.is_admin
430 })
431 end
432
433 def right_delete_multiple(
434 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
435 %{
436 "permission_group" => permission_group,
437 "nicknames" => nicknames
438 }
439 )
440 when permission_group in ["moderator", "admin"] do
441 with false <- Enum.member?(nicknames, admin_nickname) do
442 update = %{:"is_#{permission_group}" => false}
443
444 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
445
446 for u <- users, do: User.admin_api_update(u, update)
447
448 ModerationLog.insert_log(%{
449 action: "revoke",
450 actor: admin,
451 subject: users,
452 permission: permission_group
453 })
454
455 json(conn, update)
456 else
457 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
458 end
459 end
460
461 def right_delete_multiple(conn, _) do
462 render_error(conn, :not_found, "No such permission_group")
463 end
464
465 def right_delete(
466 %{assigns: %{user: admin}} = conn,
467 %{
468 "permission_group" => permission_group,
469 "nickname" => nickname
470 }
471 )
472 when permission_group in ["moderator", "admin"] do
473 fields = %{:"is_#{permission_group}" => false}
474
475 {:ok, user} =
476 nickname
477 |> User.get_cached_by_nickname()
478 |> User.admin_api_update(fields)
479
480 ModerationLog.insert_log(%{
481 action: "revoke",
482 actor: admin,
483 subject: [user],
484 permission: permission_group
485 })
486
487 json(conn, fields)
488 end
489
490 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
491 render_error(conn, :forbidden, "You can't revoke your own admin status.")
492 end
493
494 def relay_list(conn, _params) do
495 with {:ok, list} <- Relay.list() do
496 json(conn, %{relays: list})
497 else
498 _ ->
499 conn
500 |> put_status(500)
501 end
502 end
503
504 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
505 with {:ok, _message} <- Relay.follow(target) do
506 ModerationLog.insert_log(%{
507 action: "relay_follow",
508 actor: admin,
509 target: target
510 })
511
512 json(conn, target)
513 else
514 _ ->
515 conn
516 |> put_status(500)
517 |> json(target)
518 end
519 end
520
521 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
522 with {:ok, _message} <- Relay.unfollow(target) do
523 ModerationLog.insert_log(%{
524 action: "relay_unfollow",
525 actor: admin,
526 target: target
527 })
528
529 json(conn, target)
530 else
531 _ ->
532 conn
533 |> put_status(500)
534 |> json(target)
535 end
536 end
537
538 @doc "Get a password reset token (base64 string) for given nickname"
539 def get_password_reset(conn, %{"nickname" => nickname}) do
540 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
541 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
542
543 conn
544 |> json(%{
545 token: token.token,
546 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
547 })
548 end
549
550 @doc "Force password reset for a given user"
551 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
552 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
553
554 Enum.each(users, &User.force_password_reset_async/1)
555
556 ModerationLog.insert_log(%{
557 actor: admin,
558 subject: users,
559 action: "force_password_reset"
560 })
561
562 json_response(conn, :no_content, "")
563 end
564
565 @doc "Disable mfa for user's account."
566 def disable_mfa(conn, %{"nickname" => nickname}) do
567 case User.get_by_nickname(nickname) do
568 %User{} = user ->
569 MFA.disable(user)
570 json(conn, nickname)
571
572 _ ->
573 {:error, :not_found}
574 end
575 end
576
577 @doc "Show a given user's credentials"
578 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
579 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
580 conn
581 |> put_view(AccountView)
582 |> render("credentials.json", %{user: user, for: admin})
583 else
584 _ -> {:error, :not_found}
585 end
586 end
587
588 @doc "Updates a given user"
589 def update_user_credentials(
590 %{assigns: %{user: admin}} = conn,
591 %{"nickname" => nickname} = params
592 ) do
593 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
594 {:ok, _user} <-
595 User.update_as_admin(user, params) do
596 ModerationLog.insert_log(%{
597 actor: admin,
598 subject: [user],
599 action: "updated_users"
600 })
601
602 if params["password"] do
603 User.force_password_reset_async(user)
604 end
605
606 ModerationLog.insert_log(%{
607 actor: admin,
608 subject: [user],
609 action: "force_password_reset"
610 })
611
612 json(conn, %{status: "success"})
613 else
614 {:error, changeset} ->
615 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
616
617 json(conn, %{errors: errors})
618
619 _ ->
620 json(conn, %{error: "Unable to update user."})
621 end
622 end
623
624 def list_log(conn, params) do
625 {page, page_size} = page_params(params)
626
627 log =
628 ModerationLog.get_all(%{
629 page: page,
630 page_size: page_size,
631 start_date: params["start_date"],
632 end_date: params["end_date"],
633 user_id: params["user_id"],
634 search: params["search"]
635 })
636
637 conn
638 |> put_view(ModerationLogView)
639 |> render("index.json", %{log: log})
640 end
641
642 def restart(conn, _params) do
643 with :ok <- configurable_from_database() do
644 Restarter.Pleroma.restart(Config.get(:env), 50)
645
646 json(conn, %{})
647 end
648 end
649
650 def need_reboot(conn, _params) do
651 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
652 end
653
654 defp configurable_from_database do
655 if Config.get(:configurable_from_database) do
656 :ok
657 else
658 {:error, "To use this endpoint you need to enable configuration from database."}
659 end
660 end
661
662 def reload_emoji(conn, _params) do
663 Pleroma.Emoji.reload()
664
665 conn |> json("ok")
666 end
667
668 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
669 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
670
671 User.toggle_confirmation(users)
672
673 ModerationLog.insert_log(%{
674 actor: admin,
675 subject: users,
676 action: "confirm_email"
677 })
678
679 conn |> json("")
680 end
681
682 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
683 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
684
685 User.try_send_confirmation_email(users)
686
687 ModerationLog.insert_log(%{
688 actor: admin,
689 subject: users,
690 action: "resend_confirmation_email"
691 })
692
693 conn |> json("")
694 end
695
696 def stats(conn, _) do
697 count = Stats.get_status_visibility_count()
698
699 conn
700 |> json(%{"status_visibility" => count})
701 end
702
703 defp page_params(params) do
704 {get_page(params["page"]), get_page_size(params["page_size"])}
705 end
706
707 defp get_page(page_string) when is_nil(page_string), do: 1
708
709 defp get_page(page_string) do
710 case Integer.parse(page_string) do
711 {page, _} -> page
712 :error -> 1
713 end
714 end
715
716 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
717
718 defp get_page_size(page_size_string) do
719 case Integer.parse(page_size_string) do
720 {page_size, _} -> page_size
721 :error -> @users_page_size
722 end
723 end
724 end