Email change lifecycle: request, confirm, cancel.
Implements the confirm-then-switch pattern (D-01): a verification email is sent to the new address while the old email stays active. The email only switches when the user confirms via the token link.
Security Properties
- One pending change at a time (D-04): new request cancels existing pending
- New email reserved via
pending_emailfield (D-05): blocks registration races - Token TTL: 24h configurable (D-03)
- Session invalidation on confirm (D-07): all sessions except current
- Hook integration:
:on_email_changefires at confirmation (D-52)
Summary
Functions
Cancel a pending email change.
Confirm an email change via token.
Request an email change.
Functions
@spec cancel(module(), map(), keyword()) :: {:ok, map()} | {:error, Ecto.Changeset.t()}
Cancel a pending email change.
Clears the pending_email field and deletes any pending change tokens.
Options
:changeset_fn-(user, attrs -> Ecto.Changeset.t())for clearing pending_email:token_query_fn-(user, contexts -> Ecto.Queryable.t())for token cleanup queries
Returns
{:ok, user}on success{:error, changeset}on failure
Confirm an email change via token.
Verifies the token, switches the user's email to pending_email,
clears pending_email, invalidates sessions except current, and
runs the :on_email_change hook.
Options
:find_user_by_token_fn-(repo, encoded_token -> user | nil)to look up user by change token:changeset_fn-(user, attrs -> Ecto.Changeset.t())for updating user:token_query_fn-(user, contexts -> Ecto.Queryable.t())for token cleanup queries:session_store- SessionStore for session invalidation:config- Optional config for hooks:except_token- Current session token to preserve
Returns
{:ok, user}on success:errorfor invalid or expired token
@spec request(module(), map(), String.t(), keyword()) :: {:ok, map(), String.t()} | {:error, :same_email | :email_taken | Ecto.Changeset.t()}
Request an email change.
Validates the new email is not the current email and not already taken, then creates a pending email change token.
Options
:changeset_fn-(user, attrs -> Ecto.Changeset.t())for updating pending_email:build_email_token_fn-(user, context -> {encoded_token, token_struct})to create token:token_query_fn-(user, contexts -> Ecto.Queryable.t())for token cleanup queries:email_taken_fn-(repo, email -> boolean())to check email uniqueness:config- Optional config for hooks
Returns
{:ok, user, encoded_token}on success{:error, :same_email}if new email matches current{:error, :email_taken}if email is already in use{:error, changeset}on validation failure