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