Deprecate /api/pleroma/admin/users/:nickname/toggle_activation instead of deleting it
[akkoma] / lib / pleroma / web / admin_api / admin_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 alias Pleroma.Activity
8 alias Pleroma.ModerationLog
9 alias Pleroma.Plugs.OAuthScopesPlug
10 alias Pleroma.User
11 alias Pleroma.UserInviteToken
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.Relay
14 alias Pleroma.Web.AdminAPI.AccountView
15 alias Pleroma.Web.AdminAPI.Config
16 alias Pleroma.Web.AdminAPI.ConfigView
17 alias Pleroma.Web.AdminAPI.ModerationLogView
18 alias Pleroma.Web.AdminAPI.Report
19 alias Pleroma.Web.AdminAPI.ReportView
20 alias Pleroma.Web.AdminAPI.Search
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.Endpoint
23 alias Pleroma.Web.MastodonAPI.StatusView
24 alias Pleroma.Web.Router
25
26 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
27
28 require Logger
29
30 plug(
31 OAuthScopesPlug,
32 %{scopes: ["read:accounts"]}
33 when action in [:list_users, :user_show, :right_get, :invites]
34 )
35
36 plug(
37 OAuthScopesPlug,
38 %{scopes: ["write:accounts"]}
39 when action in [
40 :get_invite_token,
41 :revoke_invite,
42 :email_invite,
43 :get_password_reset,
44 :user_follow,
45 :user_unfollow,
46 :user_delete,
47 :users_create,
48 :user_toggle_activation,
49 :user_activate,
50 :user_deactivate,
51 :tag_users,
52 :untag_users,
53 :right_add,
54 :right_delete,
55 :set_activation_status
56 ]
57 )
58
59 plug(
60 OAuthScopesPlug,
61 %{scopes: ["read:reports"]} when action in [:list_reports, :report_show]
62 )
63
64 plug(
65 OAuthScopesPlug,
66 %{scopes: ["write:reports"]}
67 when action in [:report_update_state, :report_respond]
68 )
69
70 plug(
71 OAuthScopesPlug,
72 %{scopes: ["read:statuses"]} when action == :list_user_statuses
73 )
74
75 plug(
76 OAuthScopesPlug,
77 %{scopes: ["write:statuses"]}
78 when action in [:status_update, :status_delete]
79 )
80
81 plug(
82 OAuthScopesPlug,
83 %{scopes: ["read"]}
84 when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
85 )
86
87 plug(
88 OAuthScopesPlug,
89 %{scopes: ["write"]}
90 when action in [:relay_follow, :relay_unfollow, :config_update]
91 )
92
93 @users_page_size 50
94
95 action_fallback(:errors)
96
97 def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
98 user = User.get_cached_by_nickname(nickname)
99 User.delete(user)
100
101 ModerationLog.insert_log(%{
102 actor: admin,
103 subject: user,
104 action: "delete"
105 })
106
107 conn
108 |> json(nickname)
109 end
110
111 def user_follow(%{assigns: %{user: admin}} = conn, %{
112 "follower" => follower_nick,
113 "followed" => followed_nick
114 }) do
115 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
116 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
117 User.follow(follower, followed)
118
119 ModerationLog.insert_log(%{
120 actor: admin,
121 followed: followed,
122 follower: follower,
123 action: "follow"
124 })
125 end
126
127 conn
128 |> json("ok")
129 end
130
131 def user_unfollow(%{assigns: %{user: admin}} = conn, %{
132 "follower" => follower_nick,
133 "followed" => followed_nick
134 }) do
135 with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
136 %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
137 User.unfollow(follower, followed)
138
139 ModerationLog.insert_log(%{
140 actor: admin,
141 followed: followed,
142 follower: follower,
143 action: "unfollow"
144 })
145 end
146
147 conn
148 |> json("ok")
149 end
150
151 def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
152 changesets =
153 Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
154 user_data = %{
155 nickname: nickname,
156 name: nickname,
157 email: email,
158 password: password,
159 password_confirmation: password,
160 bio: "."
161 }
162
163 User.register_changeset(%User{}, user_data, need_confirmation: false)
164 end)
165 |> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
166 Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
167 end)
168
169 case Pleroma.Repo.transaction(changesets) do
170 {:ok, users} ->
171 res =
172 users
173 |> Map.values()
174 |> Enum.map(fn user ->
175 {:ok, user} = User.post_register_action(user)
176
177 user
178 end)
179 |> Enum.map(&AccountView.render("created.json", %{user: &1}))
180
181 ModerationLog.insert_log(%{
182 actor: admin,
183 subjects: Map.values(users),
184 action: "create"
185 })
186
187 conn
188 |> json(res)
189
190 {:error, id, changeset, _} ->
191 res =
192 Enum.map(changesets.operations, fn
193 {current_id, {:changeset, _current_changeset, _}} when current_id == id ->
194 AccountView.render("create-error.json", %{changeset: changeset})
195
196 {_, {:changeset, current_changeset, _}} ->
197 AccountView.render("create-error.json", %{changeset: current_changeset})
198 end)
199
200 conn
201 |> put_status(:conflict)
202 |> json(res)
203 end
204 end
205
206 def user_show(conn, %{"nickname" => nickname}) do
207 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
208 conn
209 |> put_view(AccountView)
210 |> render("show.json", %{user: user})
211 else
212 _ -> {:error, :not_found}
213 end
214 end
215
216 def list_user_statuses(conn, %{"nickname" => nickname} = params) do
217 godmode = params["godmode"] == "true" || params["godmode"] == true
218
219 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
220 {_, page_size} = page_params(params)
221
222 activities =
223 ActivityPub.fetch_user_activities(user, nil, %{
224 "limit" => page_size,
225 "godmode" => godmode
226 })
227
228 conn
229 |> put_view(StatusView)
230 |> render("index.json", %{activities: activities, as: :activity})
231 else
232 _ -> {:error, :not_found}
233 end
234 end
235
236 def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
237 user = User.get_cached_by_nickname(nickname)
238
239 {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
240
241 action = if user.info.deactivated, do: "activate", else: "deactivate"
242
243 ModerationLog.insert_log(%{
244 actor: admin,
245 subject: [user],
246 action: action
247 })
248
249 conn
250 |> put_view(AccountView)
251 |> render("show.json", %{user: updated_user})
252 end
253
254 def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
255 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
256 {:ok, updated_users} = User.deactivate(users, false)
257
258 ModerationLog.insert_log(%{
259 actor: admin,
260 subject: users,
261 action: "activate"
262 })
263
264 conn
265 |> put_view(AccountView)
266 |> render("index.json", %{users: Keyword.values(updated_users)})
267 end
268
269 def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
270 users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
271 {:ok, updated_users} = User.deactivate(users, true)
272
273 ModerationLog.insert_log(%{
274 actor: admin,
275 subject: users,
276 action: "deactivate"
277 })
278
279 conn
280 |> put_view(AccountView)
281 |> render("index.json", %{users: Keyword.values(updated_users)})
282 end
283
284 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
285 with {:ok, _} <- User.tag(nicknames, tags) do
286 ModerationLog.insert_log(%{
287 actor: admin,
288 nicknames: nicknames,
289 tags: tags,
290 action: "tag"
291 })
292
293 json_response(conn, :no_content, "")
294 end
295 end
296
297 def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
298 with {:ok, _} <- User.untag(nicknames, tags) do
299 ModerationLog.insert_log(%{
300 actor: admin,
301 nicknames: nicknames,
302 tags: tags,
303 action: "untag"
304 })
305
306 json_response(conn, :no_content, "")
307 end
308 end
309
310 def list_users(conn, params) do
311 {page, page_size} = page_params(params)
312 filters = maybe_parse_filters(params["filters"])
313
314 search_params = %{
315 query: params["query"],
316 page: page,
317 page_size: page_size,
318 tags: params["tags"],
319 name: params["name"],
320 email: params["email"]
321 }
322
323 with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
324 do:
325 conn
326 |> json(
327 AccountView.render("index.json",
328 users: users,
329 count: count,
330 page_size: page_size
331 )
332 )
333 end
334
335 @filters ~w(local external active deactivated is_admin is_moderator)
336
337 @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
338 defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
339
340 defp maybe_parse_filters(filters) do
341 filters
342 |> String.split(",")
343 |> Enum.filter(&Enum.member?(@filters, &1))
344 |> Enum.map(&String.to_atom(&1))
345 |> Enum.into(%{}, &{&1, true})
346 end
347
348 def right_add(%{assigns: %{user: admin}} = conn, %{
349 "permission_group" => permission_group,
350 "nicknames" => nicknames
351 })
352 when permission_group in ["moderator", "admin"] do
353 info = Map.put(%{}, "is_" <> permission_group, true)
354
355 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
356
357 User.update_info(users, &User.Info.admin_api_update(&1, info))
358
359 ModerationLog.insert_log(%{
360 action: "grant",
361 actor: admin,
362 subject: users,
363 permission: permission_group
364 })
365
366 json(conn, info)
367 end
368
369 def right_add(conn, _) do
370 render_error(conn, :not_found, "No such permission_group")
371 end
372
373 def right_get(conn, %{"nickname" => nickname}) do
374 user = User.get_cached_by_nickname(nickname)
375
376 conn
377 |> json(%{
378 is_moderator: user.info.is_moderator,
379 is_admin: user.info.is_admin
380 })
381 end
382
383 def right_delete(
384 %{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
385 %{
386 "permission_group" => permission_group,
387 "nicknames" => nicknames
388 }
389 )
390 when permission_group in ["moderator", "admin"] do
391 with false <- Enum.member?(nicknames, admin_nickname) do
392 info = Map.put(%{}, "is_" <> permission_group, false)
393
394 users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
395
396 User.update_info(users, &User.Info.admin_api_update(&1, info))
397
398 ModerationLog.insert_log(%{
399 action: "revoke",
400 actor: admin,
401 subject: users,
402 permission: permission_group
403 })
404
405 json(conn, info)
406 else
407 _ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
408 end
409 end
410
411 def right_delete(conn, _) do
412 render_error(conn, :not_found, "No such permission_group")
413 end
414
415 def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
416 with {:ok, _message} <- Relay.follow(target) do
417 ModerationLog.insert_log(%{
418 action: "relay_follow",
419 actor: admin,
420 target: target
421 })
422
423 json(conn, target)
424 else
425 _ ->
426 conn
427 |> put_status(500)
428 |> json(target)
429 end
430 end
431
432 def relay_unfollow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do
433 with {:ok, _message} <- Relay.unfollow(target) do
434 ModerationLog.insert_log(%{
435 action: "relay_unfollow",
436 actor: admin,
437 target: target
438 })
439
440 json(conn, target)
441 else
442 _ ->
443 conn
444 |> put_status(500)
445 |> json(target)
446 end
447 end
448
449 @doc "Sends registration invite via email"
450 def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
451 with true <-
452 Pleroma.Config.get([:instance, :invites_enabled]) &&
453 !Pleroma.Config.get([:instance, :registrations_open]),
454 {:ok, invite_token} <- UserInviteToken.create_invite(),
455 email <-
456 Pleroma.Emails.UserEmail.user_invitation_email(
457 user,
458 invite_token,
459 email,
460 params["name"]
461 ),
462 {:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
463 json_response(conn, :no_content, "")
464 end
465 end
466
467 @doc "Create an account registration invite token"
468 def create_invite_token(conn, params) do
469 opts = %{}
470
471 opts =
472 if params["max_use"],
473 do: Map.put(opts, :max_use, params["max_use"]),
474 else: opts
475
476 opts =
477 if params["expires_at"],
478 do: Map.put(opts, :expires_at, params["expires_at"]),
479 else: opts
480
481 {:ok, invite} = UserInviteToken.create_invite(opts)
482
483 json(conn, AccountView.render("invite.json", %{invite: invite}))
484 end
485
486 @doc "Get list of created invites"
487 def invites(conn, _params) do
488 invites = UserInviteToken.list_invites()
489
490 conn
491 |> put_view(AccountView)
492 |> render("invites.json", %{invites: invites})
493 end
494
495 @doc "Revokes invite by token"
496 def revoke_invite(conn, %{"token" => token}) do
497 with {:ok, invite} <- UserInviteToken.find_by_token(token),
498 {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
499 conn
500 |> put_view(AccountView)
501 |> render("invite.json", %{invite: updated_invite})
502 else
503 nil -> {:error, :not_found}
504 end
505 end
506
507 @doc "Get a password reset token (base64 string) for given nickname"
508 def get_password_reset(conn, %{"nickname" => nickname}) do
509 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
510 {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
511
512 conn
513 |> json(%{
514 token: token.token,
515 link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
516 })
517 end
518
519 @doc "Force password reset for a given user"
520 def force_password_reset(conn, %{"nickname" => nickname}) do
521 (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
522
523 User.force_password_reset_async(user)
524
525 json_response(conn, :no_content, "")
526 end
527
528 def list_reports(conn, params) do
529 {page, page_size} = page_params(params)
530
531 params =
532 params
533 |> Map.put("type", "Flag")
534 |> Map.put("skip_preload", true)
535 |> Map.put("total", true)
536 |> Map.put("limit", page_size)
537 |> Map.put("offset", (page - 1) * page_size)
538
539 reports = ActivityPub.fetch_activities([], params, :offset)
540
541 conn
542 |> put_view(ReportView)
543 |> render("index.json", %{reports: reports})
544 end
545
546 def report_show(conn, %{"id" => id}) do
547 with %Activity{} = report <- Activity.get_by_id(id) do
548 conn
549 |> put_view(ReportView)
550 |> render("show.json", Report.extract_report_info(report))
551 else
552 _ -> {:error, :not_found}
553 end
554 end
555
556 def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state" => state}) do
557 with {:ok, report} <- CommonAPI.update_report_state(id, state) do
558 ModerationLog.insert_log(%{
559 action: "report_update",
560 actor: admin,
561 subject: report
562 })
563
564 conn
565 |> put_view(ReportView)
566 |> render("show.json", Report.extract_report_info(report))
567 end
568 end
569
570 def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do
571 with false <- is_nil(params["status"]),
572 %Activity{} <- Activity.get_by_id(id) do
573 params =
574 params
575 |> Map.put("in_reply_to_status_id", id)
576 |> Map.put("visibility", "direct")
577
578 {:ok, activity} = CommonAPI.post(user, params)
579
580 ModerationLog.insert_log(%{
581 action: "report_response",
582 actor: user,
583 subject: activity,
584 text: params["status"]
585 })
586
587 conn
588 |> put_view(StatusView)
589 |> render("show.json", %{activity: activity})
590 else
591 true ->
592 {:param_cast, nil}
593
594 nil ->
595 {:error, :not_found}
596 end
597 end
598
599 def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
600 with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
601 {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
602
603 ModerationLog.insert_log(%{
604 action: "status_update",
605 actor: admin,
606 subject: activity,
607 sensitive: sensitive,
608 visibility: params["visibility"]
609 })
610
611 conn
612 |> put_view(StatusView)
613 |> render("show.json", %{activity: activity})
614 end
615 end
616
617 def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
618 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
619 ModerationLog.insert_log(%{
620 action: "status_delete",
621 actor: user,
622 subject_id: id
623 })
624
625 json(conn, %{})
626 end
627 end
628
629 def list_log(conn, params) do
630 {page, page_size} = page_params(params)
631
632 log =
633 ModerationLog.get_all(%{
634 page: page,
635 page_size: page_size,
636 start_date: params["start_date"],
637 end_date: params["end_date"],
638 user_id: params["user_id"],
639 search: params["search"]
640 })
641
642 conn
643 |> put_view(ModerationLogView)
644 |> render("index.json", %{log: log})
645 end
646
647 def migrate_to_db(conn, _params) do
648 Mix.Tasks.Pleroma.Config.run(["migrate_to_db"])
649 json(conn, %{})
650 end
651
652 def migrate_from_db(conn, _params) do
653 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"])
654 json(conn, %{})
655 end
656
657 def config_show(conn, _params) do
658 configs = Pleroma.Repo.all(Config)
659
660 conn
661 |> put_view(ConfigView)
662 |> render("index.json", %{configs: configs})
663 end
664
665 def config_update(conn, %{"configs" => configs}) do
666 updated =
667 if Pleroma.Config.get([:instance, :dynamic_configuration]) do
668 updated =
669 Enum.map(configs, fn
670 %{"group" => group, "key" => key, "delete" => "true"} = params ->
671 {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]})
672 config
673
674 %{"group" => group, "key" => key, "value" => value} ->
675 {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value})
676 config
677 end)
678 |> Enum.reject(&is_nil(&1))
679
680 Pleroma.Config.TransferTask.load_and_update_env()
681 Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"])
682 updated
683 else
684 []
685 end
686
687 conn
688 |> put_view(ConfigView)
689 |> render("index.json", %{configs: updated})
690 end
691
692 def reload_emoji(conn, _params) do
693 Pleroma.Emoji.reload()
694
695 conn |> json("ok")
696 end
697
698 def errors(conn, {:error, :not_found}) do
699 conn
700 |> put_status(:not_found)
701 |> json(dgettext("errors", "Not found"))
702 end
703
704 def errors(conn, {:error, reason}) do
705 conn
706 |> put_status(:bad_request)
707 |> json(reason)
708 end
709
710 def errors(conn, {:param_cast, _}) do
711 conn
712 |> put_status(:bad_request)
713 |> json(dgettext("errors", "Invalid parameters"))
714 end
715
716 def errors(conn, _) do
717 conn
718 |> put_status(:internal_server_error)
719 |> json(dgettext("errors", "Something went wrong"))
720 end
721
722 defp page_params(params) do
723 {get_page(params["page"]), get_page_size(params["page_size"])}
724 end
725
726 defp get_page(page_string) when is_nil(page_string), do: 1
727
728 defp get_page(page_string) do
729 case Integer.parse(page_string) do
730 {page, _} -> page
731 :error -> 1
732 end
733 end
734
735 defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
736
737 defp get_page_size(page_size_string) do
738 case Integer.parse(page_size_string) do
739 {page_size, _} -> page_size
740 :error -> @users_page_size
741 end
742 end
743 end