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