Allow to edit account email and delete account
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -20,7 +20,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
|
||||
%{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} <- Events.create_feed_token(%{user_id: id, actor_id: actor_id}) do
|
||||
{:ok, feed_token}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
@@ -33,7 +33,7 @@ defmodule Mobilizon.GraphQL.Resolvers.FeedToken do
|
||||
"""
|
||||
@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
|
||||
with {:ok, feed_token} <- Events.create_feed_token(%{user_id: id}) do
|
||||
{:ok, feed_token}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
|
||||
alias Mobilizon.{Actors, Config, Events, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Crypto
|
||||
alias Mobilizon.Storage.Repo
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@@ -14,6 +15,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
|
||||
require Logger
|
||||
|
||||
@confirmation_token_length 30
|
||||
|
||||
@doc """
|
||||
Find an user by its ID
|
||||
"""
|
||||
@@ -298,4 +301,82 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
def change_password(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to change your password"}
|
||||
end
|
||||
|
||||
def change_email(_parent, %{email: new_email, password: password}, %{
|
||||
context: %{current_user: %User{email: old_email, password_hash: password_hash} = user}
|
||||
}) do
|
||||
with {:current_password, true} <-
|
||||
{:current_password, Argon2.verify_pass(password, password_hash)},
|
||||
{:same_email, false} <- {:same_email, new_email == old_email},
|
||||
{:email_valid, true} <- {:email_valid, Email.Checker.valid?(new_email)},
|
||||
{:ok, %User{} = user} <-
|
||||
user
|
||||
|> User.changeset(%{
|
||||
unconfirmed_email: new_email,
|
||||
confirmation_token: Crypto.random_string(@confirmation_token_length),
|
||||
confirmation_sent_at: DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
})
|
||||
|> Repo.update() do
|
||||
user
|
||||
|> Email.User.send_email_reset_old_email()
|
||||
|> Email.Mailer.deliver_later()
|
||||
|
||||
user
|
||||
|> Email.User.send_email_reset_new_email()
|
||||
|> Email.Mailer.deliver_later()
|
||||
|
||||
{:ok, user}
|
||||
else
|
||||
{:current_password, false} ->
|
||||
{:error, "The password provided is invalid"}
|
||||
|
||||
{:same_email, true} ->
|
||||
{:error, "The new email must be different"}
|
||||
|
||||
{:email_valid, _} ->
|
||||
{:error, "The new email doesn't seem to be valid"}
|
||||
end
|
||||
end
|
||||
|
||||
def change_email(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to change your email"}
|
||||
end
|
||||
|
||||
def validate_email(_parent, %{token: token}, _resolution) do
|
||||
with %User{} = user <- Users.get_user_by_activation_token(token),
|
||||
{:ok, %User{} = user} <-
|
||||
user
|
||||
|> User.changeset(%{
|
||||
email: user.unconfirmed_email,
|
||||
unconfirmed_email: nil,
|
||||
confirmation_token: nil,
|
||||
confirmation_sent_at: nil
|
||||
})
|
||||
|> Repo.update() do
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
{:current_password, false} ->
|
||||
{:error, "The password provided is invalid"}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_account(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to delete your account"}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -178,5 +178,21 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
arg(:new_password, non_null(:string))
|
||||
resolve(&User.change_password/3)
|
||||
end
|
||||
|
||||
field :change_email, :user do
|
||||
arg(:email, non_null(:string))
|
||||
arg(:password, non_null(:string))
|
||||
resolve(&User.change_email/3)
|
||||
end
|
||||
|
||||
field :validate_email, :user do
|
||||
arg(:token, non_null(:string))
|
||||
resolve(&User.validate_email/3)
|
||||
end
|
||||
|
||||
field :delete_account, :deleted_object do
|
||||
arg(:password, non_null(:string))
|
||||
resolve(&User.delete_account/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -186,7 +186,7 @@ defmodule Mobilizon.Actors do
|
||||
%Actor{}
|
||||
|> Actor.registration_changeset(args)
|
||||
|> Repo.insert() do
|
||||
Events.create_feed_token(%{"user_id" => args["user_id"], "actor_id" => person.id})
|
||||
Events.create_feed_token(%{user_id: args["user_id"], actor_id: person.id})
|
||||
|
||||
{:ok, person}
|
||||
end
|
||||
|
||||
@@ -1367,7 +1367,7 @@ defmodule Mobilizon.Events do
|
||||
"""
|
||||
@spec create_feed_token(map) :: {:ok, FeedToken.t()} | {:error, Changeset.t()}
|
||||
def create_feed_token(attrs \\ %{}) do
|
||||
attrs = Map.put(attrs, "token", Ecto.UUID.generate())
|
||||
attrs = Map.put(attrs, :token, Ecto.UUID.generate())
|
||||
|
||||
%FeedToken{}
|
||||
|> FeedToken.changeset(attrs)
|
||||
|
||||
@@ -39,11 +39,12 @@ defmodule Mobilizon.Users.User do
|
||||
:confirmation_token,
|
||||
:reset_password_sent_at,
|
||||
:reset_password_token,
|
||||
:locale
|
||||
:locale,
|
||||
:unconfirmed_email
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@registration_required_attrs [:email, :password]
|
||||
@registration_required_attrs @required_attrs ++ [:password]
|
||||
|
||||
@password_change_required_attrs [:password]
|
||||
@password_reset_required_attrs @password_change_required_attrs ++
|
||||
@@ -61,6 +62,7 @@ defmodule Mobilizon.Users.User do
|
||||
field(:confirmation_token, :string)
|
||||
field(:reset_password_sent_at, :utc_datetime)
|
||||
field(:reset_password_token, :string)
|
||||
field(:unconfirmed_email, :string)
|
||||
field(:locale, :string, default: "en")
|
||||
|
||||
belongs_to(:default_actor, Actor)
|
||||
@@ -99,7 +101,7 @@ defmodule Mobilizon.Users.User do
|
||||
|> save_confirmation_token()
|
||||
|> unique_constraint(
|
||||
:confirmation_token,
|
||||
message: "The registration is already in use, this looks like an issue on our side."
|
||||
message: "The registration token is already in use, this looks like an issue on our side."
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ defmodule Mobilizon.Users do
|
||||
%User{}
|
||||
|> User.registration_changeset(args)
|
||||
|> Repo.insert() do
|
||||
Events.create_feed_token(%{"user_id" => user.id})
|
||||
Events.create_feed_token(%{user_id: user.id})
|
||||
|
||||
{:ok, user}
|
||||
end
|
||||
@@ -267,7 +267,10 @@ defmodule Mobilizon.Users do
|
||||
|
||||
@spec user_by_email_query(String.t(), boolean | nil) :: Ecto.Query.t()
|
||||
defp user_by_email_query(email, nil) do
|
||||
from(u in User, where: u.email == ^email, preload: :default_actor)
|
||||
from(u in User,
|
||||
where: u.email == ^email or u.unconfirmed_email == ^email,
|
||||
preload: :default_actor
|
||||
)
|
||||
end
|
||||
|
||||
defp user_by_email_query(email, true) do
|
||||
@@ -281,7 +284,7 @@ defmodule Mobilizon.Users do
|
||||
defp user_by_email_query(email, false) do
|
||||
from(
|
||||
u in User,
|
||||
where: u.email == ^email and is_nil(u.confirmed_at),
|
||||
where: (u.email == ^email or u.unconfirmed_email == ^email) and is_nil(u.confirmed_at),
|
||||
preload: :default_actor
|
||||
)
|
||||
end
|
||||
|
||||
@@ -61,9 +61,10 @@ defmodule Mobilizon.Web.Email.User do
|
||||
with %User{} = user <- Users.get_user_by_activation_token(token),
|
||||
{:ok, %User{} = user} <-
|
||||
Users.update_user(user, %{
|
||||
"confirmed_at" => DateTime.utc_now() |> DateTime.truncate(:second),
|
||||
"confirmation_sent_at" => nil,
|
||||
"confirmation_token" => nil
|
||||
confirmed_at: DateTime.utc_now() |> DateTime.truncate(:second),
|
||||
confirmation_sent_at: nil,
|
||||
confirmation_token: nil,
|
||||
email: user.unconfirmed_email || user.email
|
||||
}) do
|
||||
Logger.info("User #{user.email} has been confirmed")
|
||||
{:ok, user}
|
||||
@@ -141,6 +142,48 @@ defmodule Mobilizon.Web.Email.User do
|
||||
end
|
||||
end
|
||||
|
||||
def send_email_reset_old_email(
|
||||
%User{locale: user_locale, email: email, unconfirmed_email: unconfirmed_email} = _user,
|
||||
_locale \\ "en"
|
||||
) do
|
||||
Gettext.put_locale(user_locale)
|
||||
|
||||
subject =
|
||||
gettext(
|
||||
"Mobilizon on %{instance}: email changed",
|
||||
instance: Config.instance_name()
|
||||
)
|
||||
|
||||
Email.base_email(to: email, subject: subject)
|
||||
|> assign(:locale, user_locale)
|
||||
|> assign(:subject, subject)
|
||||
|> assign(:new_email, unconfirmed_email)
|
||||
|> render(:email_changed_old)
|
||||
end
|
||||
|
||||
def send_email_reset_new_email(
|
||||
%User{
|
||||
locale: user_locale,
|
||||
unconfirmed_email: unconfirmed_email,
|
||||
confirmation_token: confirmation_token
|
||||
} = _user,
|
||||
_locale \\ "en"
|
||||
) do
|
||||
Gettext.put_locale(user_locale)
|
||||
|
||||
subject =
|
||||
gettext(
|
||||
"Mobilizon on %{instance}: confirm your email address",
|
||||
instance: Config.instance_name()
|
||||
)
|
||||
|
||||
Email.base_email(to: unconfirmed_email, subject: subject)
|
||||
|> assign(:locale, user_locale)
|
||||
|> assign(:subject, subject)
|
||||
|> assign(:token, confirmation_token)
|
||||
|> render(:email_changed_new)
|
||||
end
|
||||
|
||||
@spec we_can_send_email(User.t(), atom) :: :ok | {:error, :email_too_soon}
|
||||
defp we_can_send_email(%User{} = user, key) do
|
||||
case Map.get(user, key) do
|
||||
|
||||
@@ -130,6 +130,8 @@ defmodule Mobilizon.Web.Router do
|
||||
as: "participation_email_confirmation"
|
||||
)
|
||||
|
||||
get("/validate/email/:token", PageController, :index, as: "user_email_validation")
|
||||
|
||||
get("/interact", PageController, :interact)
|
||||
end
|
||||
|
||||
|
||||
75
lib/web/templates/email/email_changed_new.html.eex
Normal file
75
lib/web/templates/email/email_changed_new.html.eex
Normal file
@@ -0,0 +1,75 @@
|
||||
<!-- HERO -->
|
||||
<tr>
|
||||
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "Verify email address" %>
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- COPY BLOCK -->
|
||||
<tr>
|
||||
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<!-- COPY -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= gettext "Confirm the new address to change your email." %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="#424056">
|
||||
<a href="<%= user_email_validation_url(Mobilizon.Web.Endpoint, :index, @token) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #424056; display: inline-block;">
|
||||
<%= gettext "Verify email address" %>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
|
||||
<p style="margin: 0">
|
||||
<%= gettext "If this change wasn't initiated by you, please ignore this email. The email address for the Mobilizon account won't change until you access the link above." %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
10
lib/web/templates/email/email_changed_new.text.eex
Normal file
10
lib/web/templates/email/email_changed_new.text.eex
Normal file
@@ -0,0 +1,10 @@
|
||||
<%= gettext "Verify email address" %>
|
||||
|
||||
==
|
||||
|
||||
<%= gettext "Confirm the new address to change your email." %>
|
||||
|
||||
<%= user_email_validation_url(Mobilizon.Web.Endpoint, :index, @token) %>
|
||||
|
||||
|
||||
<%= gettext "If this change wasn't initiated by you, please ignore this email. The email address for the Mobilizon account won't change until you access the link above." %>
|
||||
73
lib/web/templates/email/email_changed_old.html.eex
Normal file
73
lib/web/templates/email/email_changed_old.html.eex
Normal file
@@ -0,0 +1,73 @@
|
||||
<!-- HERO -->
|
||||
<tr>
|
||||
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "New email address" %>
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- COPY BLOCK -->
|
||||
<tr>
|
||||
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<!-- COPY -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= gettext "The email address for your account on %{host} is being changed to:", host: @instance[:name] %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<%= @new_email %>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
|
||||
<p style="margin: 0">
|
||||
<%= gettext "If you did not ask to change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account." %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
9
lib/web/templates/email/email_changed_old.text.eex
Normal file
9
lib/web/templates/email/email_changed_old.text.eex
Normal file
@@ -0,0 +1,9 @@
|
||||
<%= gettext "New email address" %>
|
||||
|
||||
==
|
||||
|
||||
<%= gettext "The email address for your account on %{host} is being changed to:", host: @instance[:name] %>
|
||||
|
||||
<%= @new_email %>
|
||||
|
||||
<%= gettext "If you did not ask to change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account." %>
|
||||
Reference in New Issue
Block a user