Allow to edit account email and delete account

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-02-13 15:48:12 +01:00
parent 9fc3c7017f
commit 9f007da286
44 changed files with 2262 additions and 590 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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>

View 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." %>

View 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>

View 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." %>