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