Skip to content

Commit

Permalink
Keep confirmation token during password update.
Browse files Browse the repository at this point in the history
When user decides to change password, mail can still be waiting confirmation.

Reason why user changes password is not important and
does not lead to any security issue related to email confirmation.
So related part of logic is the same for update and reset of password.
  • Loading branch information
ShPakvel committed Oct 12, 2024
1 parent 60d090f commit 430cd21
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 20 deletions.
19 changes: 9 additions & 10 deletions priv/templates/phx.gen.auth/context_functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,7 @@
|> <%= inspect schema.alias %>.password_changeset(attrs)
|> <%= inspect schema.alias %>.validate_current_password(password)

Ecto.Multi.new()
|> Ecto.Multi.update(:<%= schema.singular %>, changeset)
|> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
|> Repo.transaction()
|> case do
{:ok, %{<%= schema.singular %>: <%= schema.singular %>}} -> {:ok, <%= schema.singular %>}
{:error, :<%= schema.singular %>, changeset, _} -> {:error, changeset}
end
update_password(<%= schema.singular %>, changeset)
end

## Session
Expand Down Expand Up @@ -336,9 +329,15 @@
"""
def reset_<%= schema.singular %>_password(<%= schema.singular %>, attrs) do
update_password(<%= schema.singular %>, <%= inspect schema.alias %>.password_changeset(<%= schema.singular %>, attrs))
end

defp update_password(<%= schema.singular %>, changeset) do
tokens_query = <%= inspect schema.alias %>Token.by_<%= schema.singular %>_except_contexts_query(<%= schema.singular %>, ["confirm"])

Ecto.Multi.new()
|> Ecto.Multi.update(:<%= schema.singular %>, <%= inspect schema.alias %>.password_changeset(<%= schema.singular %>, attrs))
|> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
|> Ecto.Multi.update(:<%= schema.singular %>, changeset)
|> Ecto.Multi.delete_all(:tokens, tokens_query)
|> Repo.transaction()
|> case do
{:ok, %{<%= schema.singular %>: <%= schema.singular %>}} -> {:ok, <%= schema.singular %>}
Expand Down
14 changes: 12 additions & 2 deletions priv/templates/phx.gen.auth/schema_token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,22 @@ defmodule <%= inspect schema.module %>Token do
end

@doc """
Gets all tokens for the given <%= schema.singular %> for the given contexts.
Gets all tokens for the given <%= schema.singular %>.
"""
def by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all) do
def by_<%= schema.singular %>_query(<%= schema.singular %>) do
from t in <%= inspect schema.alias %>Token, where: t.<%= schema.singular %>_id == ^<%= schema.singular %>.id
end

@doc """
Gets all tokens for the given <%= schema.singular %> except the given contexts.
"""
def by_<%= schema.singular %>_except_contexts_query(<%= schema.singular %>, [_ | _] = contexts) do
from t in <%= inspect schema.alias %>Token, where: t.<%= schema.singular %>_id == ^<%= schema.singular %>.id and t.context not in ^contexts
end

@doc """
Gets all tokens for the given <%= schema.singular %> for the given contexts.
"""
def by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, [_ | _] = contexts) do
from t in <%= inspect schema.alias %>Token, where: t.<%= schema.singular %>_id == ^<%= schema.singular %>.id and t.context in ^contexts
end
Expand Down
26 changes: 18 additions & 8 deletions priv/templates/phx.gen.auth/test_cases.exs
Original file line number Diff line number Diff line change
Expand Up @@ -202,16 +202,21 @@
end
test "updates the email with a valid token", %{<%= schema.singular %>: <%= schema.singular %>, token: token, email: email} do
<%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, & &1)
<%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, & &1)
assert <%= inspect context.alias %>.update_<%= schema.singular %>_email(<%= schema.singular %>, token) == :ok
changed_<%= schema.singular %> = Repo.get!(<%= inspect schema.alias %>, <%= schema.singular %>.id)
assert changed_<%= schema.singular %>.email != <%= schema.singular %>.email
assert changed_<%= schema.singular %>.email == email
assert changed_<%= schema.singular %>.confirmed_at
assert changed_<%= schema.singular %>.confirmed_at != <%= schema.singular %>.confirmed_at
refute Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
end
test "deletes all tokens sent to email for the given <%= schema.singular %>", %{<%= schema.singular %>: <%= schema.singular %>, token: token} do
_ = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
_ = <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, & &1)
_ = <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, & &1)
:ok = <%= inspect context.alias %>.update_<%= schema.singular %>_email(<%= schema.singular %>, token)
assert [%<%= inspect schema.alias %>Token{context: "session"}] = Repo.all(<%= inspect schema.alias %>Token.by_<%= schema.singular %>_query(<%= schema.singular %>))
end
test "does not update email with invalid token", %{<%= schema.singular %>: <%= schema.singular %>} do
Expand Down Expand Up @@ -296,15 +301,17 @@
assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, "new valid password")
end
test "deletes all tokens for the given <%= schema.singular %>", %{<%= schema.singular %>: <%= schema.singular %>} do
test "deletes all tokens except confirmation for the given <%= schema.singular %>", %{<%= schema.singular %>: <%= schema.singular %>} do
_ = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
_ = <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, & &1)
_ = <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, & &1)
{:ok, _} =
<%= inspect context.alias %>.update_<%= schema.singular %>_password(<%= schema.singular %>, valid_<%= schema.singular %>_password(), %{
password: "new valid password"
})
refute Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
assert [%<%= inspect schema.alias %>Token{context: "confirm"}] = Repo.all(<%= inspect schema.alias %>Token.by_<%= schema.singular %>_query(<%= schema.singular %>))
end
end
Expand Down Expand Up @@ -491,10 +498,13 @@
assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email_and_password(<%= schema.singular %>.email, "new valid password")
end

test "deletes all tokens for the given <%= schema.singular %>", %{<%= schema.singular %>: <%= schema.singular %>} do
test "deletes all tokens except confirmation for the given <%= schema.singular %>", %{<%= schema.singular %>: <%= schema.singular %>} do
_ = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
_ = <%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, & &1)
_ = <%= inspect context.alias %>.deliver_<%= schema.singular %>_confirmation_instructions(<%= schema.singular %>, & &1)

{:ok, _} = <%= inspect context.alias %>.reset_<%= schema.singular %>_password(<%= schema.singular %>, %{password: "new valid password"})
refute Repo.get_by(<%= inspect schema.alias %>Token, <%= schema.singular %>_id: <%= schema.singular %>.id)
assert [%<%= inspect schema.alias %>Token{context: "confirm"}] = Repo.all(<%= inspect schema.alias %>Token.by_<%= schema.singular %>_query(<%= schema.singular %>))
end
end

Expand Down

0 comments on commit 430cd21

Please sign in to comment.