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