Merge branch 'feature/gen-magic' into 'develop'
[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.Stats
14 alias Pleroma.User
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Builder
17 alias Pleroma.Web.ActivityPub.Pipeline
18 alias Pleroma.Web.AdminAPI
19 alias Pleroma.Web.AdminAPI.AccountView
20 alias Pleroma.Web.AdminAPI.ModerationLogView
21 alias Pleroma.Web.AdminAPI.Search
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Web.Plugs.OAuthScopesPlug
24 alias Pleroma.Web.Router
25
26 @users_page_size 50
27
28 plug(
29 OAuthScopesPlug,
30 %{scopes: ["read:accounts"], admin: true}
31 when action in [:list_users, :user_show, :right_get, :show_user_credentials]
32 )
33
34 plug(
35 OAuthScopesPlug,
36 %{scopes: ["write:accounts"], admin: true}
37 when action in [
38 :get_password_reset,
39 :force_password_reset,
40 :user_delete,
41 :users_create,
42 :user_toggle_activation,
43 :user_activate,
44 :user_deactivate,
45 :user_approve,
46 :tag_users,
47 :untag_users,
48 :right_add,
49 :right_add_multiple,
50 :right_delete,
51 :disable_mfa,
52 :right_delete_multiple,
53 :update_user_credentials
54 ]
55 )
56
57 plug(
58 OAuthScopesPlug,
59 %{scopes: ["write:follows"], admin: true}
60 when action in [:user_follow, :user_unfollow]
61 )
62
63 plug(
64 OAuthScopesPlug,
65 %{scopes: ["read:statuses"], admin: true}
66 when action in [:list_user_statuses, :list_instance_statuses]
67 )
68
69 plug(
70 OAuthScopesPlug,
71 %{scopes: ["read:chats"], admin: true}
72 when action in [:list_user_chats]
73 )
74
75 plug(
76 OAuthScopesPlug,
77 %{scopes: ["read"], admin: true}
78 when action in [
79 :list_log,
80 :stats,
81 :need_reboot
82 ]
83 )
84
85 plug(
86 OAuthScopesPlug,
87 %{scopes: ["write"], admin: true}
88 when action in [
89 :restart,
90 :resend_confirmation_email,
91 :confirm_email,
92 :reload_emoji
93 ]
94 )
95
96 action_fallback(AdminAPI.FallbackController)
97
98 def user_delete(conn, %{"nickname" => nickname}) do
99 user_delete(conn, %{"nicknames" => [nickname]})
100 end
101
102 def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
103 users =
104 nicknames
105 |> Enum.map(&User.get_cached_by_nickname/1)
106
107 users
108 |> Enum.each(fn user ->
109 {:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
110 Pipeline.common_pipeline(delete_data, local: true)
111 end)
112
113 ModerationLog.insert_log(%{
114 actor: admin,
115 subject: users,
116 action: "delete"
117 })
118
119 json(conn, nicknames)
120 end
121
122 def user_follow(%{assigns: %{user: admin}} = conn, %{
123 "follower" => follower_nick,
124 "followed" => followed_nick
125 }) do
126 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
127 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
128 User.follow(follower, followed)
129
130 ModerationLog.insert_log(%{
131 actor: admin,
132 followed: followed,
133 follower: follower,
134 action: "follow"
135 })
136 end
137
138 json(conn, "ok")
139 end
140
141 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
142 "follower" => follower_nick,
143 "followed" => followed_nick
144 }) do
145 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
146 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
147 User.unfollow(follower, followed)
148
149 ModerationLog.insert_log(%{
150 actor: admin,
151 followed: followed,
152 follower: follower,
153 action: "unfollow"
154 })
155 end
156
157 json(conn, "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 json(conn, res)
197
198 {:error, id, changeset, _} ->
199 res =
200 Enum.map(changesets.operations, fn
201 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
202 AccountView.render("create-error.json", %{changeset: changeset})
203
204 {_, {:changeset, current_changeset, _}} ->
205 AccountView.render("create-error.json", %{changeset: current_changeset})
206 end)
207
208 conn
209 |> put_status(:conflict)
210 |> json(res)
211 end
212 end
213
214 def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
215 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
216 conn
217 |> put_view(AccountView)
218 |> render("show.json", %{user: user})
219 else
220 _ -> {:error, :not_found}
221 end
222 end
223
224 def list_instance_statuses(conn, %{"instance" => instance} = params) do
225 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
226 {page, page_size} = page_params(params)
227
228 activities =
229 ActivityPub.fetch_statuses(nil, %{
230 instance: instance,
231 limit: page_size,
232 offset: (page - 1) * page_size,
233 exclude_reblogs: not with_reblogs
234 })
235
236 conn
237 |> put_view(AdminAPI.StatusView)
238 |> render("index.json", %{activities: activities, as: :activity})
239 end
240
241 def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
242 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
243 godmode = params["godmode"] == "true" || params["godmode"] == true
244
245 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
246 {_, page_size} = page_params(params)
247
248 activities =
249 ActivityPub.fetch_user_activities(user, nil, %{
250 limit: page_size,
251 godmode: godmode,
252 exclude_reblogs: not with_reblogs
253 })
254
255 conn
256 |> put_view(AdminAPI.StatusView)
257 |> render("index.json", %{activities: activities, as: :activity})
258 else
259 _ -> {:error, :not_found}
260 end
261 end
262
263 def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do
264 with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
265 chats =
266 Pleroma.Chat.for_user_query(user_id)
267 |> Pleroma.Repo.all()
268
269 conn
270 |> put_view(AdminAPI.ChatView)
271 |> render("index.json", chats: chats)
272 else
273 _ -> {:error, :not_found}
274 end
275 end
276
277 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
278 user = User.get_cached_by_nickname(nickname)
279
280 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
281
282 action = if user.deactivated, do: "activate", else: "deactivate"
283
284 ModerationLog.insert_log(%{
285 actor: admin,
286 subject: [user],
287 action: action
288 })
289
290 conn
291 |> put_view(AccountView)
292 |> render("show.json", %{user: updated_user})
293 end
294
295 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
296 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
297 {:ok, updated_users} = User.deactivate(users, false)
298
299 ModerationLog.insert_log(%{
300 actor: admin,
301 subject: users,
302 action: "activate"
303 })
304
305 conn
306 |> put_view(AccountView)
307 |> render("index.json", %{users: Keyword.values(updated_users)})
308 end
309
310 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
311 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
312 {:ok, updated_users} = User.deactivate(users, true)
313
314 ModerationLog.insert_log(%{
315 actor: admin,
316 subject: users,
317 action: "deactivate"
318 })
319
320 conn
321 |> put_view(AccountView)
322 |> render("index.json", %{users: Keyword.values(updated_users)})
323 end
324
325 def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
326 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
327 {:ok, updated_users} = User.approve(users)
328
329 ModerationLog.insert_log(%{
330 actor: admin,
331 subject: users,
332 action: "approve"
333 })
334
335 conn
336 |> put_view(AccountView)
337 |> render("index.json", %{users: updated_users})
338 end
339
340 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
341 with {:ok, _} <- User.tag(nicknames, tags) do
342 ModerationLog.insert_log(%{
343 actor: admin,
344 nicknames: nicknames,
345 tags: tags,
346 action: "tag"
347 })
348
349 json_response(conn, :no_content, "")
350 end
351 end
352
353 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
354 with {:ok, _} <- User.untag(nicknames, tags) do
355 ModerationLog.insert_log(%{
356 actor: admin,
357 nicknames: nicknames,
358 tags: tags,
359 action: "untag"
360 })
361
362 json_response(conn, :no_content, "")
363 end
364 end
365
366 def list_users(conn, params) do
367 {page, page_size} = page_params(params)
368 filters = maybe_parse_filters(params["filters"])
369
370 search_params = %{
371 query: params["query"],
372 page: page,
373 page_size: page_size,
374 tags: params["tags"],
375 name: params["name"],
376 email: params["email"]
377 }
378
379 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) 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 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 {get_page(params["page"]), get_page_size(params["page_size"])}
686 end
687
688 defp get_page(page_string) when is_nil(page_string), do: 1
689
690 defp get_page(page_string) do
691 case Integer.parse(page_string) do
692 {page, _} -> page
693 :error -> 1
694 end
695 end
696
697 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
698
699 defp get_page_size(page_size_string) do
700 case Integer.parse(page_size_string) do
701 {page_size, _} -> page_size
702 :error -> @users_page_size
703 end
704 end
705 end