Introduce basic user and profile management

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-06-11 19:13:21 +02:00
parent da4ea84baf
commit beb35a09c6
51 changed files with 1808 additions and 254 deletions

View File

@@ -16,6 +16,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
alias Mobilizon.Service.Statistics
alias Mobilizon.Storage.Page
alias Mobilizon.Users.User
require Logger
def list_action_logs(
_parent,
@@ -96,6 +97,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
}
end
defp transform_action_log(Actor, :suspend, %ActionLog{changes: changes}) do
%{
action: :actor_suspension,
object: convert_changes_to_struct(Actor, changes)
}
end
defp transform_action_log(Actor, :unsuspend, %ActionLog{changes: changes}) do
%{
action: :actor_unsuspension,
object: convert_changes_to_struct(Actor, changes)
}
end
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}),

View File

@@ -3,10 +3,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
Handles the person-related GraphQL calls
"""
import Mobilizon.Users.Guards
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin
alias Mobilizon.Events
alias Mobilizon.Events.Participant
alias Mobilizon.Storage.Page
alias Mobilizon.Users
alias Mobilizon.Users.User
@@ -17,8 +21,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
Get a person
"""
def get_person(_parent, %{id: id}, _resolution) do
with %Actor{} = actor <- Actors.get_actor_with_preload(id),
def get_person(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
with %Actor{suspended: suspended} = actor <- Actors.get_actor_with_preload(id, true),
true <- suspended == false or is_moderator(role),
actor <- proxify_pictures(actor) do
{:ok, actor}
else
@@ -41,6 +46,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end
end
def list_persons(
_parent,
%{
preferred_username: preferred_username,
name: name,
domain: domain,
local: local,
suspended: suspended,
page: page,
limit: limit
},
%{
context: %{current_user: %User{role: role}}
}
)
when is_moderator(role) do
{:ok,
Actors.list_actors(:Person, preferred_username, name, domain, local, suspended, page, limit)}
end
def list_persons(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to list persons"}
end
@doc """
Returns the current actor for the currently logged-in user
"""
@@ -201,25 +230,41 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
{:no_participant, {:ok, %Participant{} = participant}} <-
{:no_participant, Events.get_participant(event_id, actor_id)} do
{:ok, [participant]}
{:ok, %Page{elements: [participant], total: 1}}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:no_participant, _} ->
{:ok, []}
{:ok, %Page{elements: [], total: 0}}
end
end
@doc """
Returns the list of events this person is going to
"""
def person_participations(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
participations <- Events.list_event_participations_for_actor(actor) do
{:ok, participations}
def person_participations(%Actor{id: actor_id} = actor, %{page: page, limit: limit}, %{
context: %{current_user: %User{role: role} = user}
}) do
{:is_owned, actor_found} = User.owns_actor(user, actor_id)
res =
cond do
not is_nil(actor_found) ->
true
is_moderator(role) ->
true
true ->
false
end
with {:is_owned, true} <- {:is_owned, res},
%Page{} = page <- Events.list_event_participations_for_actor(actor, page, limit) do
{:ok, page}
else
{:is_owned, nil} ->
{:is_owned, false} ->
{:error, "Actor id is not owned by authenticated user"}
end
end
@@ -243,6 +288,95 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|> proxify_banner
end
def user_for_person(%Actor{type: :Person, user_id: user_id}, _args, %{
context: %{current_user: %User{role: role}}
})
when is_moderator(role) do
with false <- is_nil(user_id),
%User{} = user <- Users.get_user(user_id) do
{:ok, user}
else
true ->
{:ok, nil}
_ ->
{:error, "User not found"}
end
end
def user_for_person(_, _args, _resolution), do: {:error, nil}
def organized_events_for_person(
%Actor{user_id: actor_user_id} = actor,
%{page: page, limit: limit},
%{
context: %{current_user: %User{id: user_id, role: role}}
}
) do
with true <- actor_user_id == user_id or is_moderator(role),
%Page{} = page <- Events.list_organized_events_for_actor(actor, page, limit) do
{:ok, page}
end
end
def suspend_profile(_parent, %{id: id}, %{
context: %{current_user: %User{role: role} = user}
})
when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{suspended: false} = actor <- Actors.get_remote_actor_with_preload(id),
{:ok, _} <- Actors.delete_actor(actor),
{:ok, _} <- Admin.log_action(moderator_actor, "suspend", actor) do
{:ok, actor}
else
{:moderator_actor, nil} ->
{:error, "No actor found for the moderator user"}
%Actor{suspended: true} ->
{:error, "Actor already suspended"}
nil ->
{:error, "No remote profile found with this ID"}
{:error, _} ->
{:error, "Error while performing background task"}
end
end
def suspend_profile(_parent, _args, _resolution) do
{:error, "Only moderators and administrators can suspend a profile"}
end
def unsuspend_profile(_parent, %{id: id}, %{
context: %{current_user: %User{role: role} = user}
})
when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <-
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{preferred_username: preferred_username, domain: domain} = actor <-
Actors.get_remote_actor_with_preload(id, true),
{:ok, _} <- Actors.update_actor(actor, %{suspended: false}),
{:ok, %Actor{} = actor} <-
ActivityPub.make_actor_from_nickname("#{preferred_username}@#{domain}"),
{:ok, _} <- Admin.log_action(moderator_actor, "unsuspend", actor) do
{:ok, actor}
else
{:moderator_actor, nil} ->
{:error, "No actor found for the moderator user"}
nil ->
{:error, "No remote profile found with this ID"}
{:error, _} ->
{:error, "Error while performing background task"}
end
end
def unsuspend_profile(_parent, _args, _resolution) do
{:error, "Only moderators and administrators can unsuspend a profile"}
end
# We check that the actor is not the last administrator/creator of a group
@spec last_admin_of_a_group?(integer()) :: boolean()
defp last_admin_of_a_group?(actor_id) do

View File

@@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
alias Mobilizon.{Actors, Config, Events, Users}
alias Mobilizon.Actors.Actor
alias Mobilizon.Crypto
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.{Setting, User}
@@ -20,8 +21,11 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """
Find an user by its ID
"""
def find_user(_parent, %{id: id}, _resolution) do
Users.get_user_with_actors(id)
def find_user(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do
with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do
{:ok, user}
end
end
@doc """
@@ -38,19 +42,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """
List instance users
"""
def list_and_count_users(
def list_users(
_parent,
%{page: page, limit: limit, sort: sort, direction: direction},
%{email: email, page: page, limit: limit, sort: sort, direction: direction},
%{context: %{current_user: %User{role: role}}}
)
when is_moderator(role) do
total = Task.async(&Users.count_users/0)
elements = Task.async(fn -> Users.list_users(page, limit, sort, direction) end)
{:ok, %{total: Task.await(total), elements: Task.await(elements)}}
{:ok, Users.list_users(email, page, limit, sort, direction)}
end
def list_and_count_users(_parent, _args, _resolution) do
def list_users(_parent, _args, _resolution) do
{:error, "You need to have admin access to list users"}
end
@@ -242,9 +243,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
def user_participations(
%User{id: user_id},
args,
%{context: %{current_user: %User{id: logged_user_id}}}
%{context: %{current_user: %User{id: logged_user_id, role: role}}}
) do
with true <- user_id == logged_user_id,
with true <- user_id == logged_user_id or is_moderator(role),
%Page{} = page <-
Events.list_participations_for_user(
user_id,
@@ -379,27 +380,58 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
def delete_account(_parent, %{password: password}, %{
context: %{current_user: %User{password_hash: password_hash} = user}
}) do
with {:current_password, true} <-
{:current_password, Argon2.verify_pass(password, password_hash)},
actors <- Users.get_actors_for_user(user),
# Detach actors from user
:ok <- Enum.each(actors, fn actor -> Actors.update_actor(actor, %{user_id: nil}) end),
# Launch a background job to delete actors
:ok <- Enum.each(actors, &Actors.delete_actor/1),
# Delete user
{:ok, user} <- Users.delete_user(user) do
{:ok, user}
else
case {:current_password, Argon2.verify_pass(password, password_hash)} do
{:current_password, true} ->
do_delete_account(user)
{:current_password, false} ->
{:error, "The password provided is invalid"}
end
end
def delete_account(_parent, %{user_id: user_id}, %{
context: %{current_user: %User{role: role}}
})
when is_moderator(role) do
with %User{} = user <- Users.get_user(user_id) do
do_delete_account(%User{} = user)
end
end
def delete_account(_parent, _args, _resolution) do
{:error, "You need to be logged-in to delete your account"}
end
defp do_delete_account(%User{} = user) do
with actors <- Users.get_actors_for_user(user),
activated <- not is_nil(user.confirmed_at),
# Detach actors from user
:ok <-
if(activated,
do: :ok,
else: Enum.each(actors, fn actor -> Actors.update_actor(actor, %{user_id: nil}) end)
),
# Launch a background job to delete actors
:ok <-
Enum.each(actors, fn actor ->
ActivityPub.delete(actor, true)
end),
# Delete user
{:ok, user} <- Users.delete_user(user, reserve_email: activated) do
{:ok, user}
end
end
@spec user_settings(User.t(), map(), map()) :: {:ok, list(Setting.t())} | {:error, String.t()}
def user_settings(%User{} = user, _args, %{
context: %{current_user: %User{role: role}}
})
when is_moderator(role) do
with {:setting, settings} <- {:setting, Users.get_setting(user)} do
{:ok, settings}
end
end
def user_settings(%User{id: user_id} = user, _args, %{
context: %{current_user: %User{id: logged_user_id}}
}) do

View File

@@ -17,9 +17,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
Represents a person identity
"""
object :person do
interfaces([:actor])
interfaces([:actor, :action_log_object])
field(:id, :id, description: "Internal ID for this person")
field(:user, :user, description: "The user this actor is associated to")
field(:user, :user,
description: "The user this actor is associated to",
resolve: &Person.user_for_person/3
)
field(:member_of, list_of(:member), description: "The list of groups this person is member of")
@@ -52,16 +56,21 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
)
# This one should have a privacy setting
field(:organized_events, list_of(:event),
resolve: dataloader(Events),
field(:organized_events, :paginated_event_list,
description: "A list of the events this actor has organized"
)
) do
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Person.organized_events_for_person/3)
end
@desc "The list of events this person goes to"
field(:participations, list_of(:participant),
field(:participations, :paginated_participant_list,
description: "The list of events this person goes to"
) do
arg(:event_id, :id)
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Person.person_participations/3)
end
@@ -95,6 +104,17 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
field :identities, list_of(:person) do
resolve(&Person.identities/3)
end
field :persons, :persons do
arg(:preferred_username, :string, default_value: "")
arg(:name, :string, default_value: "")
arg(:domain, :string, default_value: "")
arg(:local, :boolean, default_value: true)
arg(:suspended, :boolean, default_value: false)
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Person.list_persons/3)
end
end
object :person_mutations do
@@ -168,6 +188,16 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
resolve(handle_errors(&Person.register_person/3))
end
field :suspend_profile, :deleted_object do
arg(:id, :id, description: "The profile ID to suspend")
resolve(&Person.suspend_profile/3)
end
field :unsuspend_profile, :person do
arg(:id, :id, description: "The profile ID to unsuspend")
resolve(&Person.unsuspend_profile/3)
end
end
object :person_subscriptions do

View File

@@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
use Absinthe.Schema.Notation
alias Mobilizon.Actors.Actor
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Reports.{Note, Report}
@@ -29,6 +30,8 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
value(:event_deletion)
value(:comment_deletion)
value(:event_update)
value(:actor_suspension)
value(:actor_unsuspension)
end
@desc "The objects that can be in an action log"
@@ -48,6 +51,9 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
%Comment{}, _ ->
:comment
%Actor{type: "Person"}, _ ->
:person
_, _ ->
nil
end)

View File

@@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field(:id, non_null(:id), description: "The user's ID")
field(:email, non_null(:string), description: "The user's email")
field(:profiles, non_null(list_of(:person)),
field(:actors, non_null(list_of(:person)),
description: "The user's list of profiles (identities)"
)
@@ -51,6 +51,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field(:locale, :string, description: "The user's locale")
field(:disabled, :boolean, description: "Whether the user is disabled")
field(:participations, :paginated_participant_list,
description: "The list of participations this user has"
) do
@@ -144,13 +146,14 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "List instance users"
field :users, :users do
arg(:email, :string, default_value: "")
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
arg(:sort, :sortable_user_field, default_value: :id)
arg(:direction, :sort_direction, default_value: :desc)
resolve(&User.list_and_count_users/3)
resolve(&User.list_users/3)
end
end
@@ -233,7 +236,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "Delete an account"
field :delete_account, :deleted_object do
arg(:password, non_null(:string))
arg(:password, :string)
arg(:user_id, :id)
resolve(&User.delete_account/3)
end