Split GraphQL as separate context

This commit is contained in:
rustra
2020-01-26 20:34:25 +01:00
parent a7acf17a4a
commit ba3ad713c0
55 changed files with 292 additions and 269 deletions

View File

@@ -0,0 +1,46 @@
defmodule Mobilizon.GraphQL.Resolvers.Address do
@moduledoc """
Handles the comment-related GraphQL calls
"""
alias Mobilizon.Addresses.Address
alias Mobilizon.Service.Geospatial
require Logger
@doc """
Search an address
"""
@spec search(map, map, map) :: {:ok, [Address.t()]}
def search(
_parent,
%{query: query, locale: locale, page: _page, limit: _limit},
%{context: %{ip: ip}}
) do
geolix = Geolix.lookup(ip)
country_code =
case geolix do
%{country: %{iso_code: country_code}} -> String.downcase(country_code)
_ -> nil
end
addresses = Geospatial.service().search(query, lang: locale, country_code: country_code)
{:ok, addresses}
end
@doc """
Reverse geocode some coordinates
"""
@spec reverse_geocode(map, map, map) :: {:ok, [Address.t()]}
def reverse_geocode(
_parent,
%{longitude: longitude, latitude: latitude, zoom: zoom, locale: locale},
_context
) do
addresses = Geospatial.service().geocode(longitude, latitude, lang: locale, zoom: zoom)
{:ok, addresses}
end
end

View File

@@ -0,0 +1,214 @@
defmodule Mobilizon.GraphQL.Resolvers.Admin do
@moduledoc """
Handles the report-related GraphQL calls.
"""
import Mobilizon.Users.Guards
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.ActionLog
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User
alias Mobilizon.Service.Statistics
alias Mobilizon.Storage.Page
alias Mobilizon.Federation.ActivityPub.Relay
def list_action_logs(
_parent,
%{page: page, limit: limit},
%{context: %{current_user: %User{role: role}}}
)
when is_moderator(role) do
with action_logs <- Mobilizon.Admin.list_action_logs(page, limit) do
action_logs =
action_logs
|> Enum.map(fn %ActionLog{
target_type: target_type,
action: action,
actor: actor,
id: id,
inserted_at: inserted_at
} = action_log ->
with data when is_map(data) <-
transform_action_log(String.to_existing_atom(target_type), action, action_log) do
Map.merge(data, %{actor: actor, id: id, inserted_at: inserted_at})
end
end)
|> Enum.filter(& &1)
{:ok, action_logs}
end
end
def list_action_logs(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to list action logs"}
end
defp transform_action_log(
Report,
:update,
%ActionLog{} = action_log
) do
with %Report{} = report <- Mobilizon.Reports.get_report(action_log.target_id) do
action =
case action_log do
%ActionLog{changes: %{"status" => "closed"}} -> :report_update_closed
%ActionLog{changes: %{"status" => "open"}} -> :report_update_opened
%ActionLog{changes: %{"status" => "resolved"}} -> :report_update_resolved
end
%{
action: action,
object: report
}
end
end
defp transform_action_log(Note, :create, %ActionLog{changes: changes}) do
%{
action: :note_creation,
object: convert_changes_to_struct(Note, changes)
}
end
defp transform_action_log(Note, :delete, %ActionLog{changes: changes}) do
%{
action: :note_deletion,
object: convert_changes_to_struct(Note, changes)
}
end
defp transform_action_log(Event, :delete, %ActionLog{changes: changes}) do
%{
action: :event_deletion,
object: convert_changes_to_struct(Event, changes)
}
end
defp transform_action_log(Comment, :delete, %ActionLog{changes: changes}) do
%{
action: :comment_deletion,
object: convert_changes_to_struct(Comment, 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}),
data <- Map.put(data, :report, Mobilizon.Reports.get_report(data.report_id)) do
struct(struct, data)
end
end
defp convert_changes_to_struct(struct, changes) do
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}) do
struct(struct, data)
end
end
def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
last_public_event_published =
case Events.list_events(1, 1, :inserted_at, :desc) do
[event | _] -> event
_ -> nil
end
{:ok,
%{
number_of_users: Statistics.get_cached_value(:local_users),
number_of_events: Statistics.get_cached_value(:local_events),
number_of_comments: Statistics.get_cached_value(:local_comments),
number_of_reports: Mobilizon.Reports.count_opened_reports(),
last_public_event_published: last_public_event_published
}}
end
def get_dashboard(_parent, _args, _resolution) do
{:error, "You need to be logged-in and an administrator to access dashboard statistics"}
end
def list_relay_followers(
_parent,
%{page: page, limit: limit},
%{context: %{current_user: %User{role: role}}}
)
when is_admin(role) do
with %Actor{} = relay_actor <- Relay.get_actor() do
%Page{} =
page = Actors.list_external_followers_for_actor_paginated(relay_actor, page, limit)
{:ok, page}
end
end
def list_relay_followings(
_parent,
%{page: page, limit: limit},
%{context: %{current_user: %User{role: role}}}
)
when is_admin(role) do
with %Actor{} = relay_actor <- Relay.get_actor() do
%Page{} =
page = Actors.list_external_followings_for_actor_paginated(relay_actor, page, limit)
{:ok, page}
end
end
def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
case Relay.follow(address) do
{:ok, _activity, follow} ->
{:ok, follow}
{:error, {:error, err}} when is_bitstring(err) ->
{:error, err}
end
end
def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
case Relay.unfollow(address) do
{:ok, _activity, follow} ->
{:ok, follow}
{:error, {:error, err}} when is_bitstring(err) ->
{:error, err}
end
end
def accept_subscription(
_parent,
%{address: address},
%{context: %{current_user: %User{role: role}}}
)
when is_admin(role) do
case Relay.accept(address) do
{:ok, _activity, follow} ->
{:ok, follow}
{:error, {:error, err}} when is_bitstring(err) ->
{:error, err}
end
end
def reject_subscription(
_parent,
%{address: address},
%{context: %{current_user: %User{role: role}}}
)
when is_admin(role) do
case Relay.reject(address) do
{:ok, _activity, follow} ->
{:ok, follow}
{:error, {:error, err}} when is_bitstring(err) ->
{:error, err}
end
end
end

View File

@@ -0,0 +1,78 @@
defmodule Mobilizon.GraphQL.Resolvers.Comment do
@moduledoc """
Handles the comment-related GraphQL calls.
"""
alias Mobilizon.{Actors, Admin, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Users.User
alias MobilizonWeb.API.Comments
require Logger
def get_thread(_parent, %{id: thread_id}, _context) do
{:ok, Events.get_thread_replies(thread_id)}
end
def create_comment(
_parent,
%{actor_id: actor_id} = args,
%{context: %{current_user: %User{} = user}}
) do
with {:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id),
{:ok, _, %CommentModel{} = comment} <-
Comments.create_comment(args) do
{:ok, comment}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end
def create_comment(_parent, _args, _context) do
{:error, "You are not allowed to create a comment if not connected"}
end
def delete_comment(
_parent,
%{actor_id: actor_id, comment_id: comment_id},
%{context: %{current_user: %User{role: role} = user}}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id),
%CommentModel{} = comment <- Events.get_comment_with_preload(comment_id) do
cond do
{:comment_can_be_managed, true} == CommentModel.can_be_managed_by(comment, actor_id) ->
do_delete_comment(comment)
role in [:moderator, :administrator] ->
with {:ok, res} <- do_delete_comment(comment),
%Actor{} = actor <- Actors.get_actor(actor_id) do
Admin.log_action(actor, "delete", comment)
{:ok, res}
end
true ->
{:error, "You cannot delete this comment"}
end
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end
def delete_comment(_parent, _args, %{}) do
{:error, "You are not allowed to delete a comment if not connected"}
end
defp do_delete_comment(%CommentModel{} = comment) do
with {:ok, _, %CommentModel{} = comment} <-
Comments.delete_comment(comment) do
{:ok, comment}
end
end
end

View File

@@ -0,0 +1,49 @@
defmodule Mobilizon.GraphQL.Resolvers.Config do
@moduledoc """
Handles the config-related GraphQL calls.
"""
alias Geolix.Adapter.MMDB2.Record.{Country, Location}
alias Mobilizon.Config
@doc """
Gets config.
"""
def get_config(_parent, _params, %{context: %{ip: ip}}) do
geolix = Geolix.lookup(ip)
country_code =
case Map.get(geolix, :city) do
%{country: %Country{iso_code: country_code}} -> String.downcase(country_code)
_ -> nil
end
location =
case Map.get(geolix, :city) do
%{location: %Location{} = location} -> Map.from_struct(location)
_ -> nil
end
{:ok,
%{
name: Config.instance_name(),
registrations_open: Config.instance_registrations_open?(),
registrations_whitelist: Config.instance_registrations_whitelist?(),
demo_mode: Config.instance_demo_mode?(),
description: Config.instance_description(),
location: location,
country_code: country_code,
geocoding: %{
provider: Config.instance_geocoding_provider(),
autocomplete: Config.instance_geocoding_autocomplete()
},
maps: %{
tiles: %{
endpoint: Config.instance_maps_tiles_endpoint(),
attribution: Config.instance_maps_tiles_attribution()
}
}
}}
end
end

View File

@@ -0,0 +1,367 @@
defmodule Mobilizon.GraphQL.Resolvers.Event do
@moduledoc """
Handles the event-related GraphQL calls.
"""
alias Mobilizon.{Actors, Admin, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Event, Participant, EventParticipantStats}
alias Mobilizon.Users.User
alias Mobilizon.GraphQL.Resolvers.Person
alias Mobilizon.Federation.ActivityPub.Activity
alias MobilizonWeb.API
# We limit the max number of events that can be retrieved
@event_max_limit 100
@number_of_related_events 3
def list_events(_parent, %{page: page, limit: limit}, _resolution)
when limit < @event_max_limit do
{:ok, Mobilizon.Events.list_events(page, limit)}
end
def list_events(_parent, %{page: _page, limit: _limit}, _resolution) do
{:error, :events_max_limit_reached}
end
defp find_private_event(
_parent,
%{uuid: uuid},
%{context: %{current_user: %User{id: user_id}}} = _resolution
) do
case {:has_event, Mobilizon.Events.get_own_event_by_uuid_with_preload(uuid, user_id)} do
{:has_event, %Event{} = event} ->
{:ok, Map.put(event, :organizer_actor, Person.proxify_pictures(event.organizer_actor))}
{:has_event, _} ->
{:error, "Event with UUID #{uuid} not found"}
end
end
defp find_private_event(_parent, %{uuid: uuid}, _resolution) do
{:error, "Event with UUID #{uuid} not found"}
end
def find_event(parent, %{uuid: uuid} = args, resolution) do
case {:has_event, Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid)} do
{:has_event, %Event{} = event} ->
{:ok, Map.put(event, :organizer_actor, Person.proxify_pictures(event.organizer_actor))}
{:has_event, _} ->
find_private_event(parent, args, resolution)
end
end
@doc """
List participants for event (through an event request)
"""
def list_participants_for_event(
%Event{id: event_id},
%{page: page, limit: limit, roles: roles, actor_id: actor_id},
%{context: %{current_user: %User{} = user}} = _resolution
) do
with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
# Check that moderator has right
{:actor_approve_permission, true} <-
{:actor_approve_permission, Mobilizon.Events.moderator_for_event?(event_id, actor_id)} do
roles =
case roles do
"" ->
[]
roles ->
roles
|> String.split(",")
|> Enum.map(&String.downcase/1)
|> Enum.map(&String.to_existing_atom/1)
end
{:ok, Mobilizon.Events.list_participants_for_event(event_id, roles, page, limit)}
else
{:is_owned, nil} ->
{:error, "Moderator Actor ID is not owned by authenticated user"}
{:actor_approve_permission, _} ->
{:error, "Provided moderator actor ID doesn't have permission on this event"}
end
end
def list_participants_for_event(_, _args, _resolution) do
{:ok, []}
end
def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
{:ok, stats.participant + stats.moderator + stats.administrator + stats.creator}
end
@doc """
List related events
"""
def list_related_events(
%Event{tags: tags, organizer_actor: organizer_actor, uuid: uuid},
_args,
_resolution
) do
# We get the organizer's next public event
events =
[Events.get_upcoming_public_event_for_actor(organizer_actor, uuid)]
|> Enum.filter(&is_map/1)
# We find similar events with the same tags
# uniq_by : It's possible event_from_same_actor is inside events_from_tags
events =
events
|> Enum.concat(Events.list_events_by_tags(tags, @number_of_related_events))
|> uniq_events()
# TODO: We should use tag_relations to find more appropriate events
# We've considered all recommended events, so we fetch the latest events
events =
if @number_of_related_events - length(events) > 0 do
events
|> Enum.concat(
Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true)
)
|> uniq_events()
else
events
end
events =
events
# We remove the same event from the results
|> Enum.filter(fn event -> event.uuid != uuid end)
# We return only @number_of_related_events right now
|> Enum.take(@number_of_related_events)
{:ok, events}
end
defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end)
@doc """
Join an event for an actor
"""
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
{:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, _activity, participant} <- API.Participations.join(event, actor),
participant <-
participant
|> Map.put(:event, event)
|> Map.put(:actor, Person.proxify_pictures(actor)) do
{:ok, participant}
else
{:maximum_attendee_capacity, _} ->
{:error, "The event has already reached its maximum capacity"}
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :event_not_found} ->
{:error, "Event id not found"}
{:ok, %Participant{}} ->
{:error, "You are already a participant of this event"}
end
end
def actor_join_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to join an event"}
end
@doc """
Leave an event for an actor
"""
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
{:ok, _activity, _participant} <- API.Participations.leave(event, actor) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:only_organizer, true} ->
{:error, "You can't leave event because you're the only event creator participant"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
def actor_leave_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to leave an event"}
end
def update_participation(
_parent,
%{id: participation_id, moderator_actor_id: moderator_actor_id, role: new_role},
%{context: %{current_user: user}}
) do
# Check that moderator provided is rightly authenticated
with {:is_owned, moderator_actor} <- User.owns_actor(user, moderator_actor_id),
# Check that participation already exists
{:has_participation, %Participant{role: old_role} = participation} <-
{:has_participation, Mobilizon.Events.get_participant(participation_id)},
{:same_role, false} <- {:same_role, new_role == old_role},
# Check that moderator has right
{:actor_approve_permission, true} <-
{:actor_approve_permission,
Mobilizon.Events.moderator_for_event?(participation.event.id, moderator_actor_id)},
{:ok, _activity, participation} <-
MobilizonWeb.API.Participations.update(participation, moderator_actor, new_role) do
{:ok, participation}
else
{:is_owned, nil} ->
{:error, "Moderator Actor ID is not owned by authenticated user"}
{:has_participation, %Participant{role: role, id: id}} ->
{:error,
"Participant #{id} can't be approved since it's already a participant (with role #{role})"}
{:has_participation, nil} ->
{:error, "Participant not found"}
{:actor_approve_permission, _} ->
{:error, "Provided moderator actor ID doesn't have permission on this event"}
{:same_role, true} ->
{:error, "Participant already has role #{new_role}"}
{:error, :participant_not_found} ->
{:error, "Participant not found"}
end
end
@doc """
Create an event
"""
def create_event(
_parent,
%{organizer_actor_id: organizer_actor_id} = args,
%{context: %{current_user: user}} = _resolution
) do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event}
else
{:is_owned, nil} ->
{:error, "Organizer actor id is not owned by the user"}
{:error, _, %Ecto.Changeset{} = error, _} ->
{:error, error}
{:error, %Ecto.Changeset{} = error} ->
{:error, error}
end
end
def create_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to create events"}
end
@doc """
Update an event
"""
def update_event(
_parent,
%{event_id: event_id} = args,
%{context: %{current_user: user}} = _resolution
) do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
organizer_actor_id <- args |> Map.get(:organizer_actor_id, event.organizer_actor_id),
{:is_owned, %Actor{} = organizer_actor} <-
User.owns_actor(user, organizer_actor_id),
args <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.update_event(args, event) do
{:ok, event}
else
{:error, :event_not_found} ->
{:error, "Event not found"}
{:is_owned, nil} ->
{:error, "User doesn't own actor"}
{:error, _, %Ecto.Changeset{} = error, _} ->
{:error, error}
end
end
def update_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to update an event"}
end
@doc """
Delete an event
"""
def delete_event(
_parent,
%{event_id: event_id, actor_id: actor_id},
%{context: %{current_user: %User{role: role} = user}}
) do
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id),
{actor_id, ""} <- Integer.parse(actor_id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id) do
cond do
{:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) ->
do_delete_event(event)
role in [:moderator, :administrator] ->
with {:ok, res} <- do_delete_event(event, !is_local),
%Actor{} = actor <- Actors.get_actor(actor_id) do
Admin.log_action(actor, "delete", event)
{:ok, res}
end
true ->
{:error, "You cannot delete this event"}
end
else
{:error, :event_not_found} ->
{:error, "Event not found"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end
def delete_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to delete an event"}
end
defp do_delete_event(event, federate \\ true) when is_boolean(federate) do
with {:ok, _activity, event} <- MobilizonWeb.API.Events.delete_event(event) do
{:ok, %{id: event.id}}
end
end
end

View File

@@ -0,0 +1,82 @@
defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
@moduledoc """
Handles the feed tokens-related GraphQL calls.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.FeedToken
alias Mobilizon.Users.User
require Logger
@doc """
Create an feed token for an user and a defined actor
"""
@spec create_feed_token(any, map, map) :: {:ok, FeedToken.t()} | {:error, String.t()}
def create_feed_token(
_parent,
%{actor_id: actor_id},
%{context: %{current_user: %User{id: id} = user}}
) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, feed_token} <- Events.create_feed_token(%{"user_id" => id, "actor_id" => actor_id}) do
{:ok, feed_token}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end
@doc """
Create an feed token for an user
"""
@spec create_feed_token(any, map, map) :: {:ok, FeedToken.t()}
def create_feed_token(_parent, %{}, %{context: %{current_user: %User{id: id}}}) do
with {:ok, feed_token} <- Events.create_feed_token(%{"user_id" => id}) do
{:ok, feed_token}
end
end
@spec create_feed_token(any, map, map) :: {:error, String.t()}
def create_feed_token(_parent, _args, %{}) do
{:error, "You are not allowed to create a feed token if not connected"}
end
@doc """
Delete a feed token
"""
@spec delete_feed_token(any, map, map) :: {:ok, map} | {:error, String.t()}
def delete_feed_token(
_parent,
%{token: token},
%{context: %{current_user: %User{id: id} = _user}}
) do
with {:ok, token} <- Ecto.UUID.cast(token),
{:no_token, %FeedToken{actor: actor, user: %User{} = user} = feed_token} <-
{:no_token, Events.get_feed_token(token)},
{:token_from_user, true} <- {:token_from_user, id == user.id},
{:ok, _} <- Events.delete_feed_token(feed_token) do
res = %{user: %{id: id}}
res = if is_nil(actor), do: res, else: Map.put(res, :actor, %{id: actor.id})
{:ok, res}
else
{:error, nil} ->
{:error, "No such feed token"}
:error ->
{:error, "Token is not a valid UUID"}
{:no_token, _} ->
{:error, "Token does not exist"}
{:token_from_user, false} ->
{:error, "You don't have permission to delete this token"}
end
end
@spec delete_feed_token(any, map, map) :: {:error, String.t()}
def delete_feed_token(_parent, _args, %{}) do
{:error, "You are not allowed to delete a feed token if not connected"}
end
end

View File

@@ -0,0 +1,190 @@
defmodule Mobilizon.GraphQL.Resolvers.Group do
@moduledoc """
Handles the group-related GraphQL calls.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Users.User
alias Mobilizon.GraphQL.Resolvers.Person
alias Mobilizon.Federation.ActivityPub
alias MobilizonWeb.API
require Logger
@doc """
Find a group
"""
def find_group(_parent, %{preferred_username: name}, _resolution) do
with {:ok, actor} <- ActivityPub.find_or_make_group_from_nickname(name),
actor <- Person.proxify_pictures(actor) do
{:ok, actor}
else
_ ->
{:error, "Group with name #{name} not found"}
end
end
@doc """
Lists all groups
"""
def list_groups(_parent, %{page: page, limit: limit}, _resolution) do
{
:ok,
page
|> Actors.list_groups(limit)
|> Enum.map(fn actor -> Person.proxify_pictures(actor) end)
}
end
@doc """
Create a new group. The creator is automatically added as admin
"""
def create_group(_parent, args, %{context: %{current_user: user}}) do
with creator_actor_id <- Map.get(args, :creator_actor_id),
{:is_owned, %Actor{} = creator_actor} <- User.owns_actor(user, creator_actor_id),
args <- Map.put(args, :creator_actor, creator_actor),
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.create_group(args) do
{:ok, group}
else
{:error, err} when is_bitstring(err) ->
{:error, err}
{:is_owned, nil} ->
{:error, "Creator actor id is not owned by the current user"}
end
end
def create_group(_parent, _args, _resolution) do
{:error, "You need to be logged-in to create a group"}
end
@doc """
Delete an existing group
"""
def delete_group(
_parent,
%{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- Member.is_administrator(member),
group <- Actors.delete_group!(group) do
{:ok, %{id: group.id}}
else
{:error, :group_not_found} ->
{:error, "Group not found"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :member_not_found} ->
{:error, "Actor id is not a member of this group"}
{:is_admin, false} ->
{:error, "Actor id is not an administrator of the selected group"}
end
end
def delete_group(_parent, _args, _resolution) do
{:error, "You need to be logged-in to delete a group"}
end
@doc """
Join an existing group
"""
def join_group(
_parent,
%{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
role <- Member.get_default_member_role(group),
{:ok, _} <- Actors.create_member(%{parent_id: group.id, actor_id: actor.id, role: role}) do
{
:ok,
%{
parent: Person.proxify_pictures(group),
actor: Person.proxify_pictures(actor),
role: role
}
}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :group_not_found} ->
{:error, "Group id not found"}
{:is_able_to_join, false} ->
{:error, "You cannot join this group"}
{:ok, %Member{}} ->
{:error, "You are already a member of this group"}
end
end
def join_group(_parent, _args, _resolution) do
{:error, "You need to be logged-in to join a group"}
end
@doc """
Leave a existing group
"""
def leave_group(
_parent,
%{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id),
{:only_administrator, false} <-
{:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)},
{:ok, _} <-
Mobilizon.Actors.delete_member(member) do
{:ok, %{parent: %{id: group_id}, actor: %{id: actor_id}}}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :member_not_found} ->
{:error, "Member not found"}
{:only_administrator, true} ->
{:error, "You can't leave this group because you are the only administrator"}
end
end
def leave_group(_parent, _args, _resolution) do
{:error, "You need to be logged-in to leave a group"}
end
# We check that the actor asking to leave the group is not it's only administrator
# We start by fetching the list of administrator or creators and if there's only one of them
# and that it's the actor requesting leaving the group we return true
@spec check_that_member_is_not_last_administrator(integer, integer) :: boolean
defp check_that_member_is_not_last_administrator(group_id, actor_id) do
case Actors.list_administrator_members_for_group(group_id) do
[%Member{actor: %Actor{id: member_actor_id}}] ->
actor_id == member_actor_id
_ ->
false
end
end
end

View File

@@ -0,0 +1,15 @@
defmodule Mobilizon.GraphQL.Resolvers.Member do
@moduledoc """
Handles the member-related GraphQL calls
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor}
@doc """
Find members for group
"""
def find_members_for_group(%Actor{} = actor, _args, _resolution) do
members = Actors.list_members_for_group(actor)
{:ok, members}
end
end

View File

@@ -0,0 +1,252 @@
defmodule Mobilizon.GraphQL.Resolvers.Person do
@moduledoc """
Handles the person-related GraphQL calls
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.Participant
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mobilizon.Federation.ActivityPub
@doc """
Get a person
"""
def get_person(_parent, %{id: id}, _resolution) do
with %Actor{} = actor <- Actors.get_actor_with_preload(id),
actor <- proxify_pictures(actor) do
{:ok, actor}
else
_ ->
{:error, "Person with ID #{id} not found"}
end
end
@doc """
Find a person
"""
def fetch_person(_parent, %{preferred_username: preferred_username}, _resolution) do
with {:ok, %Actor{} = actor} <-
ActivityPub.find_or_make_actor_from_nickname(preferred_username),
actor <- proxify_pictures(actor) do
{:ok, actor}
else
_ ->
{:error, "Person with username #{preferred_username} not found"}
end
end
@doc """
Returns the current actor for the currently logged-in user
"""
def get_current_person(_parent, _args, %{context: %{current_user: user}}) do
{:ok, Users.get_actor_for_user(user)}
end
def get_current_person(_parent, _args, _resolution) do
{:error, "You need to be logged-in to view current person"}
end
@doc """
Returns the list of identities for the logged-in user
"""
def identities(_parent, _args, %{context: %{current_user: user}}) do
{:ok, Users.get_actors_for_user(user)}
end
def identities(_parent, _args, _resolution) do
{:error, "You need to be logged-in to view your list of identities"}
end
@doc """
This function is used to create more identities from an existing user
"""
def create_person(
_parent,
%{preferred_username: _preferred_username} = args,
%{context: %{current_user: user}} = _resolution
) do
args = Map.put(args, :user_id, user.id)
with args <- save_attached_pictures(args),
{:ok, %Actor{} = new_person} <- Actors.new_person(args) do
{:ok, new_person}
end
end
@doc """
This function is used to create more identities from an existing user
"""
def create_person(_parent, _args, _resolution) do
{:error, "You need to be logged-in to create a new identity"}
end
@doc """
This function is used to update an existing identity
"""
def update_person(
_parent,
%{id: id} = args,
%{context: %{current_user: user}} = _resolution
) do
args = Map.put(args, :user_id, user.id)
with {:find_actor, %Actor{} = actor} <-
{:find_actor, Actors.get_actor(id)},
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
args <- save_attached_pictures(args),
{:ok, _activity, %Actor{} = actor} <- ActivityPub.update(:actor, actor, args, true) do
{:ok, actor}
else
{:find_actor, nil} ->
{:error, "Actor not found"}
{:is_owned, nil} ->
{:error, "Actor is not owned by authenticated user"}
end
end
def update_person(_parent, _args, _resolution) do
{:error, "You need to be logged-in to update an identity"}
end
@doc """
This function is used to delete an existing identity
"""
def delete_person(
_parent,
%{id: id} = _args,
%{context: %{current_user: user}} = _resolution
) do
with {:find_actor, %Actor{} = actor} <-
{:find_actor, Actors.get_actor(id)},
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
{:last_identity, false} <- {:last_identity, last_identity?(user)},
{:last_admin, false} <- {:last_admin, last_admin_of_a_group?(actor.id)},
{:ok, actor} <- Actors.delete_actor(actor) do
{:ok, actor}
else
{:find_actor, nil} ->
{:error, "Actor not found"}
{:last_identity, true} ->
{:error, "Cannot remove the last identity of a user"}
{:last_admin, true} ->
{:error, "Cannot remove the last administrator of a group"}
{:is_owned, nil} ->
{:error, "Actor is not owned by authenticated user"}
end
end
def delete_person(_parent, _args, _resolution) do
{:error, "You need to be logged-in to delete an identity"}
end
defp last_identity?(user) do
length(Users.get_actors_for_user(user)) <= 1
end
defp save_attached_pictures(args) do
Enum.reduce([:avatar, :banner], args, fn key, args ->
if Map.has_key?(args, key) && !is_nil(args[key][:picture]) do
pic = args[key][:picture]
with {:ok, %{name: name, url: url, content_type: content_type, size: _size}} <-
MobilizonWeb.Upload.store(pic.file, type: key, description: pic.alt) do
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
end
else
args
end
end)
end
@doc """
This function is used to register a person afterwards the user has been created (but not activated)
"""
def register_person(_parent, args, _resolution) do
with {:ok, %User{} = user} <- Users.get_user_by_email(args.email),
{:no_actor, nil} <- {:no_actor, Users.get_actor_for_user(user)},
args <- Map.put(args, :user_id, user.id),
args <- save_attached_pictures(args),
{:ok, %Actor{} = new_person} <- Actors.new_person(args) do
{:ok, new_person}
else
{:error, :user_not_found} ->
{:error, "No user with this email was found"}
{:no_actor, _} ->
{:error, "You already have a profile for this user"}
{:error, %Ecto.Changeset{} = e} ->
{:error, e}
end
end
@doc """
Returns the participation for a specific event
"""
def person_participations(
%Actor{id: actor_id},
%{event_id: event_id},
%{context: %{current_user: user}}
) 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]}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:no_participant, _} ->
{:ok, []}
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}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end
def proxify_pictures(%Actor{} = actor) do
actor
|> proxify_avatar
|> proxify_banner
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
length(Actors.list_group_ids_where_last_administrator(actor_id)) > 0
end
@spec proxify_avatar(Actor.t()) :: Actor.t()
defp proxify_avatar(%Actor{avatar: %{url: avatar_url} = avatar} = actor) do
actor |> Map.put(:avatar, avatar |> Map.put(:url, MobilizonWeb.MediaProxy.url(avatar_url)))
end
@spec proxify_avatar(Actor.t()) :: Actor.t()
defp proxify_avatar(%Actor{} = actor), do: actor
@spec proxify_banner(Actor.t()) :: Actor.t()
defp proxify_banner(%Actor{banner: %{url: banner_url} = banner} = actor) do
actor |> Map.put(:banner, banner |> Map.put(:url, MobilizonWeb.MediaProxy.url(banner_url)))
end
@spec proxify_banner(Actor.t()) :: Actor.t()
defp proxify_banner(%Actor{} = actor), do: actor
end

View File

@@ -0,0 +1,83 @@
defmodule Mobilizon.GraphQL.Resolvers.Picture do
@moduledoc """
Handles the picture-related GraphQL calls
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Media
alias Mobilizon.Media.Picture
alias Mobilizon.Users.User
@doc """
Get picture for an event's pic
"""
def picture(%{picture_id: picture_id} = _parent, _args, _resolution) do
with {:ok, picture} <- do_fetch_picture(picture_id), do: {:ok, picture}
end
@doc """
Get picture for an event that has an attached
See MobilizonWeb.Resolvers.Event.create_event/3
"""
def picture(%{picture: picture} = _parent, _args, _resolution), do: {:ok, picture}
def picture(_parent, %{id: picture_id}, _resolution), do: do_fetch_picture(picture_id)
def picture(_parent, _args, _resolution), do: {:ok, nil}
@spec do_fetch_picture(nil) :: {:error, nil}
defp do_fetch_picture(nil), do: {:error, nil}
@spec do_fetch_picture(String.t()) :: {:ok, Picture.t()} | {:error, :not_found}
defp do_fetch_picture(picture_id) do
case Media.get_picture(picture_id) do
%Picture{id: id, file: file} ->
{:ok,
%{
name: file.name,
url: file.url,
id: id,
content_type: file.content_type,
size: file.size
}}
_error ->
{:error, "Picture with ID #{picture_id} was not found"}
end
end
@spec upload_picture(map, map, map) :: {:ok, Picture.t()} | {:error, any}
def upload_picture(
_parent,
%{file: %Plug.Upload{} = file, actor_id: actor_id} = args,
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %{name: _name, url: url, content_type: content_type, size: size}} <-
MobilizonWeb.Upload.store(file),
args <-
args
|> Map.put(:url, url)
|> Map.put(:size, size)
|> Map.put(:content_type, content_type),
{:ok, picture = %Picture{}} <-
Media.create_picture(%{"file" => args, "actor_id" => actor_id}) do
{:ok,
%{
name: picture.file.name,
url: picture.file.url,
id: picture.id,
content_type: picture.file.content_type,
size: picture.file.size
}}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
error ->
{:error, error}
end
end
def upload_picture(_parent, _args, _resolution) do
{:error, "You need to login to upload a picture"}
end
end

View File

@@ -0,0 +1,124 @@
defmodule Mobilizon.GraphQL.Resolvers.Report do
@moduledoc """
Handles the report-related GraphQL calls.
"""
import Mobilizon.Users.Guards
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Reports
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User
alias MobilizonWeb.API.Reports, as: ReportsAPI
def list_reports(
_parent,
%{page: page, limit: limit, status: status},
%{context: %{current_user: %User{role: role}}}
)
when is_moderator(role) do
{:ok, Mobilizon.Reports.list_reports(page, limit, :updated_at, :desc, status)}
end
def list_reports(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to list reports"}
end
def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do
case Mobilizon.Reports.get_report(id) do
%Report{} = report ->
{:ok, report}
nil ->
{:error, "Report not found"}
end
end
def get_report(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to view a report"}
end
@doc """
Create a report
"""
def create_report(
_parent,
%{reporter_id: reporter_id} = args,
%{context: %{current_user: user}} = _resolution
) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, reporter_id),
{:ok, _, %Report{} = report} <- ReportsAPI.report(args) do
{:ok, report}
else
{:is_owned, nil} ->
{:error, "Reporter actor id is not owned by authenticated user"}
_error ->
{:error, "Error while saving report"}
end
end
def create_report(_parent, _args, _resolution) do
{:error, "You need to be logged-in to create reports"}
end
@doc """
Update a report's status
"""
def update_report(
_parent,
%{report_id: report_id, moderator_id: moderator_id, status: status},
%{context: %{current_user: %User{role: role} = user}}
)
when is_moderator(role) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, moderator_id),
%Report{} = report <- Mobilizon.Reports.get_report(report_id),
{:ok, %Report{} = report} <-
MobilizonWeb.API.Reports.update_report_status(actor, report, status) do
{:ok, report}
else
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
_error ->
{:error, "Error while updating report"}
end
end
def update_report(_parent, _args, _resolution) do
{:error, "You need to be logged-in and a moderator to update a report"}
end
def create_report_note(
_parent,
%{report_id: report_id, moderator_id: moderator_id, content: content},
%{context: %{current_user: %User{role: role} = user}}
)
when is_moderator(role) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Report{} = report <- Reports.get_report(report_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do
{:ok, note}
end
end
def delete_report_note(
_parent,
%{note_id: note_id, moderator_id: moderator_id},
%{context: %{current_user: %User{role: role} = user}}
)
when is_moderator(role) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Note{} = note <- Reports.get_note(note_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.delete_report_note(note, moderator) do
{:ok, %{id: note.id}}
end
end
end

View File

@@ -0,0 +1,27 @@
defmodule Mobilizon.GraphQL.Resolvers.Search do
@moduledoc """
Handles the event-related GraphQL calls
"""
alias MobilizonWeb.API.Search
@doc """
Search persons
"""
def search_persons(_parent, %{search: search, page: page, limit: limit}, _resolution) do
Search.search_actors(search, page, limit, :Person)
end
@doc """
Search groups
"""
def search_groups(_parent, %{search: search, page: page, limit: limit}, _resolution) do
Search.search_actors(search, page, limit, :Group)
end
@doc """
Search events
"""
def search_events(_parent, %{search: search, page: page, limit: limit}, _resolution) do
Search.search_events(search, page, limit)
end
end

View File

@@ -0,0 +1,48 @@
defmodule Mobilizon.GraphQL.Resolvers.Tag do
@moduledoc """
Handles the tag-related GraphQL calls
"""
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Tag}
def list_tags(_parent, %{page: page, limit: limit}, _resolution) do
tags = Mobilizon.Events.list_tags(page, limit)
{:ok, tags}
end
@doc """
Retrieve the list of tags for an event
"""
def list_tags_for_event(%Event{id: id}, _args, _resolution) do
{:ok, Mobilizon.Events.list_tags_for_event(id)}
end
@doc """
Retrieve the list of tags for an event
"""
def list_tags_for_event(%{url: url}, _args, _resolution) do
with %Event{id: event_id} <- Events.get_event_by_url(url) do
{:ok, Mobilizon.Events.list_tags_for_event(event_id)}
end
end
# @doc """
# Retrieve the list of related tags for a given tag ID
# """
# def get_related_tags(_parent, %{tag_id: tag_id}, _resolution) do
# with %Tag{} = tag <- Mobilizon.Events.get_tag!(tag_id),
# tags <- Mobilizon.Events.list_tag_neighbors(tag) do
# {:ok, tags}
# end
# end
@doc """
Retrieve the list of related tags for a parent tag
"""
def get_related_tags(%Tag{} = tag, _args, _resolution) do
with tags <- Mobilizon.Events.list_tag_neighbors(tag) do
{:ok, tags}
end
end
end

View File

@@ -0,0 +1,301 @@
defmodule Mobilizon.GraphQL.Resolvers.User do
@moduledoc """
Handles the user-related GraphQL calls.
"""
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Config, Users, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Storage.Repo
alias Mobilizon.Users.User
alias MobilizonWeb.{Auth, Email}
require Logger
@doc """
Find an user by its ID
"""
def find_user(_parent, %{id: id}, _resolution) do
Users.get_user_with_actors(id)
end
@doc """
Return current logged-in user
"""
def get_current_user(_parent, _args, %{context: %{current_user: user}}) do
{:ok, user}
end
def get_current_user(_parent, _args, _resolution) do
{:error, "You need to be logged-in to view current user"}
end
@doc """
List instance users
"""
def list_and_count_users(
_parent,
%{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)}}
end
def list_and_count_users(_parent, _args, _resolution) do
{:error, "You need to have admin access to list users"}
end
@doc """
Login an user. Returns a token and the user
"""
def login_user(_parent, %{email: email, password: password}, _resolution) do
with {:ok, %User{confirmed_at: %DateTime{}} = user} <- Users.get_user_by_email(email),
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
Users.authenticate(%{user: user, password: password}) do
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
else
{:ok, %User{confirmed_at: nil} = _user} ->
{:error, "User account not confirmed"}
{:error, :user_not_found} ->
{:error, "No user with this email was found"}
{:error, :unauthorized} ->
{:error, "Impossible to authenticate, either your email or password are invalid."}
end
end
@doc """
Refresh a token
"""
def refresh_token(_parent, %{refresh_token: refresh_token}, _context) do
with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token),
{:ok, _old, {exchanged_token, _claims}} <-
Auth.Guardian.exchange(refresh_token, ["access", "refresh"], "access"),
{:ok, refresh_token} <- Users.generate_refresh_token(user) do
{:ok, %{access_token: exchanged_token, refresh_token: refresh_token}}
else
{:error, message} ->
Logger.debug("Cannot refresh user token: #{inspect(message)}")
{:error, "Cannot refresh the token"}
end
end
def refresh_token(_parent, _params, _context) do
{:error, "You need to have an existing token to get a refresh token"}
end
@doc """
Register an user:
- check registrations are enabled
- create the user
- send a validation email to the user
"""
@spec create_user(any, map, any) :: tuple
def create_user(_parent, args, _resolution) do
with :registration_ok <- check_registration_config(args),
{:ok, %User{} = user} <- Users.register(args) do
Email.User.send_confirmation_email(user, Map.get(args, :locale, "en"))
{:ok, user}
else
:registration_closed ->
{:error, "Registrations are not enabled"}
:not_whitelisted ->
{:error, "Your email is not on the whitelist"}
error ->
error
end
end
@spec check_registration_config(map) :: atom
defp check_registration_config(%{email: email}) do
cond do
Config.instance_registrations_open?() ->
:registration_ok
Config.instance_registrations_whitelist?() ->
check_white_listed_email?(email)
true ->
:registration_closed
end
end
@spec check_white_listed_email?(String.t()) :: :registration_ok | :not_whitelisted
defp check_white_listed_email?(email) do
[_, domain] = String.split(email, "@", parts: 2, trim: true)
if domain in Config.instance_registrations_whitelist() or
email in Config.instance_registrations_whitelist(),
do: :registration_ok,
else: :not_whitelisted
end
@doc """
Validate an user, get its actor and a token
"""
def validate_user(_parent, %{token: token}, _resolution) do
with {:check_confirmation_token, {:ok, %User{} = user}} <-
{:check_confirmation_token, Email.User.check_confirmation_token(token)},
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)},
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
Users.generate_tokens(user) do
{:ok,
%{
access_token: access_token,
refresh_token: refresh_token,
user: Map.put(user, :default_actor, actor)
}}
else
error ->
Logger.info("Unable to validate user with token #{token}")
Logger.debug(inspect(error))
{:error, "Unable to validate user"}
end
end
@doc """
Send the confirmation email again.
We only do this to accounts unconfirmed
"""
def resend_confirmation_email(_parent, args, _resolution) do
with {:ok, %User{locale: locale} = user} <-
Users.get_user_by_email(Map.get(args, :email), false),
{:ok, email} <-
Email.User.resend_confirmation_email(user, Map.get(args, :locale, locale)) do
{:ok, email}
else
{:error, :user_not_found} ->
{:error, "No user to validate with this email was found"}
{:error, :email_too_soon} ->
{:error, "You requested again a confirmation email too soon"}
end
end
@doc """
Send an email to reset the password from an user
"""
def send_reset_password(_parent, args, _resolution) do
with email <- Map.get(args, :email),
{:ok, %User{locale: locale} = user} <- Users.get_user_by_email(email, true),
{:ok, %Bamboo.Email{} = _email_html} <-
Email.User.send_password_reset_email(user, Map.get(args, :locale, locale)) do
{:ok, email}
else
{:error, :user_not_found} ->
# TODO : implement rate limits for this endpoint
{:error, "No user with this email was found"}
{:error, :email_too_soon} ->
{:error, "You requested again a confirmation email too soon"}
end
end
@doc """
Reset the password from an user
"""
def reset_password(_parent, %{password: password, token: token}, _resolution) do
with {:ok, %User{} = user} <-
Email.User.check_reset_password_token(password, token),
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
Users.authenticate(%{user: user, password: password}) do
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
end
end
@doc "Change an user default actor"
def change_default_actor(
_parent,
%{preferred_username: username},
%{context: %{current_user: user}}
) do
with %Actor{id: actor_id} <- Actors.get_local_actor_by_name(username),
{:user_actor, true} <-
{:user_actor, actor_id in Enum.map(Users.get_actors_for_user(user), & &1.id)},
%User{} = user <- Users.update_user_default_actor(user.id, actor_id) do
{:ok, user}
else
{:user_actor, _} ->
{:error, :actor_not_from_user}
_error ->
{:error, :unable_to_change_default_actor}
end
end
@doc """
Returns the list of events for all of this user's identities are going to
"""
def user_participations(
%User{id: user_id},
args,
%{context: %{current_user: %User{id: logged_user_id}}}
) do
with true <- user_id == logged_user_id,
participations <-
Events.list_participations_for_user(
user_id,
Map.get(args, :after_datetime),
Map.get(args, :before_datetime),
Map.get(args, :page),
Map.get(args, :limit)
) do
{:ok, participations}
end
end
@doc """
Returns the list of draft events for the current user
"""
def user_drafted_events(
%User{id: user_id},
args,
%{context: %{current_user: %User{id: logged_user_id}}}
) do
with {:same_user, true} <- {:same_user, user_id == logged_user_id},
events <-
Events.list_drafts_for_user(user_id, Map.get(args, :page), Map.get(args, :limit)) do
{:ok, events}
end
end
def change_password(
_parent,
%{old_password: old_password, new_password: new_password},
%{context: %{current_user: %User{password_hash: old_password_hash} = user}}
) do
with {:current_password, true} <-
{:current_password, Argon2.verify_pass(old_password, old_password_hash)},
{:same_password, false} <- {:same_password, old_password == new_password},
{:ok, %User{} = user} <-
user
|> User.password_change_changeset(%{"password" => new_password})
|> Repo.update() do
{:ok, user}
else
{:current_password, false} ->
{:error, "The current password is invalid"}
{:same_password, true} ->
{:error, "The new password must be different"}
{:error, %Ecto.Changeset{errors: [password: {"registration.error.password_too_short", _}]}} ->
{:error,
"The password you have chosen is too short. Please make sure your password contains at least 6 characters."}
end
end
def change_password(_parent, _args, _resolution) do
{:error, "You need to be logged-in to change your password"}
end
end