Merge branch 'release/2.1.1' into 'stable'
[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",
365 users: users,
366 count: count,
367 page_size: page_size
368 )
369 )
370 end
371 end
372
373 @filters ~w(local external active deactivated need_approval is_admin is_moderator)
374
375 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
376 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
377
378 defp maybe_parse_filters(filters) do
379 filters
380 |> String.split(",")
381 |> Enum.filter(&Enum.member?(@filters, &1))
382 |> Map.new(&{String.to_existing_atom(&1), true})
383 end
384
385 def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
386 "permission_group" => permission_group,
387 "nicknames" => nicknames
388 })
389 when permission_group in ["moderator", "admin"] do
390 update = %{:"is_#{permission_group}" => true}
391
392 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
393
394 for u <- users, do: User.admin_api_update(u, update)
395
396 ModerationLog.insert_log(%{
397 action: "grant",
398 actor: admin,
399 subject: users,
400 permission: permission_group
401 })
402
403 json(conn, update)
404 end
405
406 def right_add_multiple(conn, _) do
407 render_error(conn, :not_found, "No such permission_group")
408 end
409
410 def right_add(%{assigns: %{user: admin}} = conn, %{
411 "permission_group" => permission_group,
412 "nickname" => nickname
413 })
414 when permission_group in ["moderator", "admin"] do
415 fields = %{:"is_#{permission_group}" => true}
416
417 {:ok, user} =
418 nickname
419 |> User.get_cached_by_nickname()
420 |> User.admin_api_update(fields)
421
422 ModerationLog.insert_log(%{
423 action: "grant",
424 actor: admin,
425 subject: [user],
426 permission: permission_group
427 })
428
429 json(conn, fields)
430 end
431
432 def right_add(conn, _) do
433 render_error(conn, :not_found, "No such permission_group")
434 end
435
436 def right_get(conn, %{"nickname" => nickname}) do
437 user = User.get_cached_by_nickname(nickname)
438
439 conn
440 |> json(%{
441 is_moderator: user.is_moderator,
442 is_admin: user.is_admin
443 })
444 end
445
446 def right_delete_multiple(
447 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
448 %{
449 "permission_group" => permission_group,
450 "nicknames" => nicknames
451 }
452 )
453 when permission_group in ["moderator", "admin"] do
454 with false <- Enum.member?(nicknames, admin_nickname) do
455 update = %{:"is_#{permission_group}" => false}
456
457 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
458
459 for u <- users, do: User.admin_api_update(u, update)
460
461 ModerationLog.insert_log(%{
462 action: "revoke",
463 actor: admin,
464 subject: users,
465 permission: permission_group
466 })
467
468 json(conn, update)
469 else
470 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
471 end
472 end
473
474 def right_delete_multiple(conn, _) do
475 render_error(conn, :not_found, "No such permission_group")
476 end
477
478 def right_delete(
479 %{assigns: %{user: admin}} = conn,
480 %{
481 "permission_group" => permission_group,
482 "nickname" => nickname
483 }
484 )
485 when permission_group in ["moderator", "admin"] do
486 fields = %{:"is_#{permission_group}" => false}
487
488 {:ok, user} =
489 nickname
490 |> User.get_cached_by_nickname()
491 |> User.admin_api_update(fields)
492
493 ModerationLog.insert_log(%{
494 action: "revoke",
495 actor: admin,
496 subject: [user],
497 permission: permission_group
498 })
499
500 json(conn, fields)
501 end
502
503 def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
504 render_error(conn, :forbidden, "You can't revoke your own admin status.")
505 end
506
507 @doc "Get a password reset token (base64 string) for given nickname"
508 def get_password_reset(conn, %{"nickname" => nickname}) do
509 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
510 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
511
512 conn
513 |> json(%{
514 token: token.token,
515 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
516 })
517 end
518
519 @doc "Force password reset for a given user"
520 def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
521 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
522
523 Enum.each(users, &User.force_password_reset_async/1)
524
525 ModerationLog.insert_log(%{
526 actor: admin,
527 subject: users,
528 action: "force_password_reset"
529 })
530
531 json_response(conn, :no_content, "")
532 end
533
534 @doc "Disable mfa for user's account."
535 def disable_mfa(conn, %{"nickname" => nickname}) do
536 case User.get_by_nickname(nickname) do
537 %User{} = user ->
538 MFA.disable(user)
539 json(conn, nickname)
540
541 _ ->
542 {:error, :not_found}
543 end
544 end
545
546 @doc "Show a given user's credentials"
547 def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
548 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
549 conn
550 |> put_view(AccountView)
551 |> render("credentials.json", %{user: user, for: admin})
552 else
553 _ -> {:error, :not_found}
554 end
555 end
556
557 @doc "Updates a given user"
558 def update_user_credentials(
559 %{assigns: %{user: admin}} = conn,
560 %{"nickname" => nickname} = params
561 ) do
562 with {_, %User{} = user} <- {:user, User.get_cached_by_nickname(nickname)},
563 {:ok, _user} <-
564 User.update_as_admin(user, params) do
565 ModerationLog.insert_log(%{
566 actor: admin,
567 subject: [user],
568 action: "updated_users"
569 })
570
571 if params["password"] do
572 User.force_password_reset_async(user)
573 end
574
575 ModerationLog.insert_log(%{
576 actor: admin,
577 subject: [user],
578 action: "force_password_reset"
579 })
580
581 json(conn, %{status: "success"})
582 else
583 {:error, changeset} ->
584 errors = Map.new(changeset.errors, fn {key, {error, _}} -> {key, error} end)
585
586 {:errors, errors}
587
588 _ ->
589 {:error, :not_found}
590 end
591 end
592
593 def list_log(conn, params) do
594 {page, page_size} = page_params(params)
595
596 log =
597 ModerationLog.get_all(%{
598 page: page,
599 page_size: page_size,
600 start_date: params["start_date"],
601 end_date: params["end_date"],
602 user_id: params["user_id"],
603 search: params["search"]
604 })
605
606 conn
607 |> put_view(ModerationLogView)
608 |> render("index.json", %{log: log})
609 end
610
611 def restart(conn, _params) do
612 with :ok <- configurable_from_database() do
613 Restarter.Pleroma.restart(Config.get(:env), 50)
614
615 json(conn, %{})
616 end
617 end
618
619 def need_reboot(conn, _params) do
620 json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
621 end
622
623 defp configurable_from_database do
624 if Config.get(:configurable_from_database) do
625 :ok
626 else
627 {:error, "To use this endpoint you need to enable configuration from database."}
628 end
629 end
630
631 def reload_emoji(conn, _params) do
632 Pleroma.Emoji.reload()
633
634 json(conn, "ok")
635 end
636
637 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
638 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
639
640 User.toggle_confirmation(users)
641
642 ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
643
644 json(conn, "")
645 end
646
647 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
648 users =
649 Enum.map(nicknames, fn nickname ->
650 nickname
651 |> User.get_cached_by_nickname()
652 |> User.send_confirmation_email()
653 end)
654
655 ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
656
657 json(conn, "")
658 end
659
660 def stats(conn, params) do
661 counters = Stats.get_status_visibility_count(params["instance"])
662
663 json(conn, %{"status_visibility" => counters})
664 end
665
666 defp page_params(params) do
667 {get_page(params["page"]), get_page_size(params["page_size"])}
668 end
669
670 defp get_page(page_string) when is_nil(page_string), do: 1
671
672 defp get_page(page_string) do
673 case Integer.parse(page_string) do
674 {page, _} -> page
675 :error -> 1
676 end
677 end
678
679 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
680
681 defp get_page_size(page_size_string) do
682 case Integer.parse(page_size_string) do
683 {page_size, _} -> page_size
684 :error -> @users_page_size
685 end
686 end
687 end