5101e28d62ed83ba83e5daf8b14f243e3f2763a0
[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 json(conn, nicknames)
115 end
116
117 def user_follow(%{assigns: %{user: admin}} = conn, %{
118 "follower" => follower_nick,
119 "followed" => followed_nick
120 }) do
121 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
122 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
123 User.follow(follower, followed)
124
125 ModerationLog.insert_log(%{
126 actor: admin,
127 followed: followed,
128 follower: follower,
129 action: "follow"
130 })
131 end
132
133 json(conn, "ok")
134 end
135
136 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
137 "follower" => follower_nick,
138 "followed" => followed_nick
139 }) do
140 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
141 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
142 User.unfollow(follower, followed)
143
144 ModerationLog.insert_log(%{
145 actor: admin,
146 followed: followed,
147 follower: follower,
148 action: "unfollow"
149 })
150 end
151
152 json(conn, "ok")
153 end
154
155 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
156 changesets =
157 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
158 user_data = %{
159 nickname: nickname,
160 name: nickname,
161 email: email,
162 password: password,
163 password_confirmation: password,
164 bio: "."
165 }
166
167 User.register_changeset(%User{}, user_data, need_confirmation: false)
168 end)
169 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
170 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
171 end)
172
173 case Pleroma.Repo.transaction(changesets) do
174 {:ok, users} ->
175 res =
176 users
177 |> Map.values()
178 |> Enum.map(fn user ->
179 {:ok, user} = User.post_register_action(user)
180
181 user
182 end)
183 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
184
185 ModerationLog.insert_log(%{
186 actor: admin,
187 subjects: Map.values(users),
188 action: "create"
189 })
190
191 json(conn, res)
192
193 {:error, id, changeset, _} ->
194 res =
195 Enum.map(changesets.operations, fn
196 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
197 AccountView.render("create-error.json", %{changeset: changeset})
198
199 {_, {:changeset, current_changeset, _}} ->
200 AccountView.render("create-error.json", %{changeset: current_changeset})
201 end)
202
203 conn
204 |> put_status(:conflict)
205 |> json(res)
206 end
207 end
208
209 def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
210 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
211 conn
212 |> put_view(AccountView)
213 |> render("show.json", %{user: user})
214 else
215 _ -> {:error, :not_found}
216 end
217 end
218
219 def list_instance_statuses(conn, %{"instance" => instance} = params) do
220 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
221 {page, page_size} = page_params(params)
222
223 activities =
224 ActivityPub.fetch_statuses(nil, %{
225 instance: instance,
226 limit: page_size,
227 offset: (page - 1) * page_size,
228 exclude_reblogs: not with_reblogs
229 })
230
231 conn
232 |> put_view(AdminAPI.StatusView)
233 |> render("index.json", %{activities: activities, as: :activity})
234 end
235
236 def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = params) do
237 with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
238 godmode = params["godmode"] == "true" || params["godmode"] == true
239
240 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
241 {_, page_size} = page_params(params)
242
243 activities =
244 ActivityPub.fetch_user_activities(user, nil, %{
245 limit: page_size,
246 godmode: godmode,
247 exclude_reblogs: not with_reblogs
248 })
249
250 conn
251 |> put_view(AdminAPI.StatusView)
252 |> render("index.json", %{activities: activities, as: :activity})
253 else
254 _ -> {:error, :not_found}
255 end
256 end
257
258 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
259 user = User.get_cached_by_nickname(nickname)
260
261 {:ok, updated_user} = User.deactivate(user, !user.deactivated)
262
263 action = if user.deactivated, do: "activate", else: "deactivate"
264
265 ModerationLog.insert_log(%{
266 actor: admin,
267 subject: [user],
268 action: action
269 })
270
271 conn
272 |> put_view(AccountView)
273 |> render("show.json", %{user: updated_user})
274 end
275
276 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
277 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
278 {:ok, updated_users} = User.deactivate(users, false)
279
280 ModerationLog.insert_log(%{
281 actor: admin,
282 subject: users,
283 action: "activate"
284 })
285
286 conn
287 |> put_view(AccountView)
288 |> render("index.json", %{users: Keyword.values(updated_users)})
289 end
290
291 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
292 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
293 {:ok, updated_users} = User.deactivate(users, true)
294
295 ModerationLog.insert_log(%{
296 actor: admin,
297 subject: users,
298 action: "deactivate"
299 })
300
301 conn
302 |> put_view(AccountView)
303 |> render("index.json", %{users: Keyword.values(updated_users)})
304 end
305
306 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
307 with {:ok, _} <- User.tag(nicknames, tags) do
308 ModerationLog.insert_log(%{
309 actor: admin,
310 nicknames: nicknames,
311 tags: tags,
312 action: "tag"
313 })
314
315 json_response(conn, :no_content, "")
316 end
317 end
318
319 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
320 with {:ok, _} <- User.untag(nicknames, tags) do
321 ModerationLog.insert_log(%{
322 actor: admin,
323 nicknames: nicknames,
324 tags: tags,
325 action: "untag"
326 })
327
328 json_response(conn, :no_content, "")
329 end
330 end
331
332 def list_users(conn, params) do
333 {page, page_size} = page_params(params)
334 filters = maybe_parse_filters(params["filters"])
335
336 search_params = %{
337 query: params["query"],
338 page: page,
339 page_size: page_size,
340 tags: params["tags"],
341 name: params["name"],
342 email: params["email"]
343 }
344
345 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
346 json(
347 conn,
348 AccountView.render("index.json",
349 users: users,
350 count: count,
351 page_size: page_size
352 )
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 |> Map.new(&{&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, for: admin) 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 {:errors, errors}
572
573 _ ->
574 {:error, :not_found}
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 json(conn, "ok")
620 end
621
622 def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
623 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
624
625 User.toggle_confirmation(users)
626
627 ModerationLog.insert_log(%{actor: admin, subject: users, action: "confirm_email"})
628
629 json(conn, "")
630 end
631
632 def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
633 users =
634 Enum.map(nicknames, fn nickname ->
635 nickname
636 |> User.get_cached_by_nickname()
637 |> User.send_confirmation_email()
638 end)
639
640 ModerationLog.insert_log(%{actor: admin, subject: users, action: "resend_confirmation_email"})
641
642 json(conn, "")
643 end
644
645 def stats(conn, params) do
646 counters = Stats.get_status_visibility_count(params["instance"])
647
648 json(conn, %{"status_visibility" => counters})
649 end
650
651 defp page_params(params) do
652 {get_page(params["page"]), get_page_size(params["page_size"])}
653 end
654
655 defp get_page(page_string) when is_nil(page_string), do: 1
656
657 defp get_page(page_string) do
658 case Integer.parse(page_string) do
659 {page, _} -> page
660 :error -> 1
661 end
662 end
663
664 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
665
666 defp get_page_size(page_size_string) do
667 case Integer.parse(page_size_string) do
668 {page_size, _} -> page_size
669 :error -> @users_page_size
670 end
671 end
672 end