Gamend.Accounts.Registration (gamend_core v1.0.1296)

Copy Markdown View Source

Creating an account and confirming it: registration, the generated username, the first-user-is-admin rule and account activation, and email confirmation.

Split out of Gamend.Accounts, which still exposes every function here under the same name.

Summary

Functions

A registration changeset for live form feedback, with the uniqueness query skipped.

Confirms a user's email by setting confirmed_at timestamp.

Confirms the unconfirmed account registered to email with the code from its confirmation email, sets its password, and returns it signed in by the caller (POST /api/v1/register/confirm).

Confirm a user by the token in the emailed link (context: "confirm").

The account an emailed confirmation link belongs to, or nil for a link that is malformed, spent or expired. It only reads: the page the link opens shows the account, and confirming waits for its button (confirm_user_by_token/1), so a mail scanner that opens every link in an email confirms nothing and spends nothing.

register_user_and_deliver/3 for a game client (POST /api/v1/register): every account it makes starts unconfirmed and is sent the email, the first one too. A client has no page to sign the first account in, and with no password it could not sign in any other way, so it confirms with the code like everyone else (confirm_user_by_code/3). It still becomes the admin.

Registers a user.

Sends the confirmation email again, with a new code, to the unconfirmed account registered to email (POST /api/v1/register/resend). Only the newest code works; links already sent keep working until they expire.

Sign-up for a visitor who is already playing on an anonymous account: the email goes on THAT account, and the confirmation email is queued exactly as for a new one. Same account id, so everything it holds stays; the link in the email signs in to it on any device and confirms the address.

Functions

change_user_registration(user, attrs \\ %{})

@spec change_user_registration(Gamend.Accounts.User.t(), map()) :: Ecto.Changeset.t()

change_user_registration_for_validation(user, attrs)

@spec change_user_registration_for_validation(Gamend.Accounts.User.t(), map()) ::
  Ecto.Changeset.t()

A registration changeset for live form feedback, with the uniqueness query skipped.

change_user_registration/2 runs unsafe_validate_unique, which is right on submit and wrong on every keystroke: the registration form's validate event is neither rate-limited nor captcha'd, so running it there turned the form into an unauthenticated oracle for "does this address have an account here?", one query per character typed. Submitting still checks, and the unique index is what actually enforces it.

Separate function rather than an option, because mix gen.sdk cannot generate a stub for a function carrying two default arguments.

confirm_user(user)

@spec confirm_user(Gamend.Accounts.User.t()) ::
  {:ok, Gamend.Accounts.User.t()} | {:error, Ecto.Changeset.t()}

Confirms a user's email by setting confirmed_at timestamp.

Examples

iex> confirm_user(user)
{:ok, %User{}}

confirm_user_by_code(email, code, password)

@spec confirm_user_by_code(String.t(), String.t(), String.t()) ::
  {:ok, {Gamend.Accounts.User.t(), [Gamend.Accounts.UserToken.t()]}}
  | {:error, :invalid_code | {:locked, pos_integer()} | Ecto.Changeset.t()}

Confirms the unconfirmed account registered to email with the code from its confirmation email, sets its password, and returns it signed in by the caller (POST /api/v1/register/confirm).

The code is the proof that the caller reads the inbox, which is why the password is set here and never at registration.

  • {:error, %Ecto.Changeset{}} for a password the account would refuse. It is checked before the code, so it costs no attempt and says nothing about the code.
  • {:error, :invalid_code} for a wrong, spent or expired code, an address with no unconfirmed account, or one already confirmed: the same answer, so it tells no one which addresses are registered.
  • {:error, {:locked, seconds}}: a wrong code counts toward the address's sign-in lockout (Gamend.Accounts.LoginLockouts), shared with passwords. The failure that locks it also voids the code, so guessing needs a fresh email for every few tries; the link keeps working.

Success revokes every earlier token of the account, as a password change does, and returns them so the caller can disconnect the sessions.

confirm_user_by_token(token)

@spec confirm_user_by_token(String.t()) ::
  {:ok, Gamend.Accounts.User.t()} | {:error, :invalid | :not_found}

Confirm a user by the token in the emailed link (context: "confirm").

Returns {:ok, user} when the token is valid and user was confirmed. Returns {:error, :not_found} or {:error, :invalid} when token is invalid/expired.

Confirming spends the link and the code sent with it. An account confirmed this way has no password: registration takes none, and one set before the address was proved is removed, since whoever set it may not own the inbox (an account registered when the API still took one, or a guest account that was given an email). Its owner sets one in settings, where the link's page signs them in, or confirms with the code instead.

deliver_user_confirmation_instructions(user, confirmation_url_fun)

@spec deliver_user_confirmation_instructions(Gamend.Accounts.User.t(), (String.t() ->
                                                                    String.t())) ::
  {:ok, Swoosh.Email.t()} | {:error, :already_confirmed | term()}

get_user_by_confirm_token(token)

@spec get_user_by_confirm_token(String.t()) :: Gamend.Accounts.User.t() | nil

The account an emailed confirmation link belongs to, or nil for a link that is malformed, spent or expired. It only reads: the page the link opens shows the account, and confirming waits for its button (confirm_user_by_token/1), so a mail scanner that opens every link in an email confirms nothing and spends nothing.

register_unconfirmed_user_and_deliver(attrs, confirmation_url_fun, notifier \\ Gamend.Accounts.UserNotifier)

@spec register_unconfirmed_user_and_deliver(
  Gamend.Types.user_registration_attrs(),
  (String.t() -> String.t()),
  module()
) :: {:ok, Gamend.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}

register_user_and_deliver/3 for a game client (POST /api/v1/register): every account it makes starts unconfirmed and is sent the email, the first one too. A client has no page to sign the first account in, and with no password it could not sign in any other way, so it confirms with the code like everyone else (confirm_user_by_code/3). It still becomes the admin.

register_user(attrs)

@spec register_user(Gamend.Types.user_registration_attrs()) ::
  {:ok, Gamend.Accounts.User.t()} | {:error, Ecto.Changeset.t()}

Registers a user.

Attributes

See Gamend.Types.user_registration_attrs/0 for available fields.

Examples

iex> register_user(%{email: "user@example.com", password: "secret123"})
{:ok, %User{}}

iex> register_user(%{email: "invalid"})
{:error, %Ecto.Changeset{}}

register_user_and_deliver(attrs, confirmation_url_fun, notifier \\ Gamend.Accounts.UserNotifier)

@spec register_user_and_deliver(
  Gamend.Types.user_registration_attrs(),
  (String.t() -> String.t()),
  module()
) :: {:ok, Gamend.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}

Register a user and queue its confirmation email.

confirmation_url_fun maps an encoded token to the confirmation URL. The email goes out from the mailers queue (Gamend.Accounts.ConfirmationMailer), enqueued in the transaction that inserts the user: the call returns once both are committed, without waiting on SMTP, and a failed send is retried there. The email carries a link and a code (confirm_user_by_code/3).

The first user becomes the admin and is confirmed, with no email: the browser form signs it in itself. A caller that cannot uses register_unconfirmed_user_and_deliver/3.

No password is taken. Whoever registers an address has not shown they own it, so a password chosen now could be someone else's, and it would sign them into the account once the address's owner confirmed it. The password is set after the inbox is proved: with the code, or in the settings the link opens.

resend_confirmation(email, confirmation_url_fun, notifier \\ Gamend.Accounts.UserNotifier)

@spec resend_confirmation(String.t(), (String.t() -> String.t()), module()) :: :ok

Sends the confirmation email again, with a new code, to the unconfirmed account registered to email (POST /api/v1/register/resend). Only the newest code works; links already sent keep working until they expire.

Always :ok, whether or not such an account exists, so it tells no one which addresses are registered. At most one email per account per minute is queued; the rest are dropped.

upgrade_anonymous_user_and_deliver(user, attrs, confirmation_url_fun, notifier \\ Gamend.Accounts.UserNotifier)

@spec upgrade_anonymous_user_and_deliver(
  Gamend.Accounts.User.t(),
  Gamend.Types.user_registration_attrs(),
  (String.t() -> String.t()),
  module()
) :: {:ok, Gamend.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}

Sign-up for a visitor who is already playing on an anonymous account: the email goes on THAT account, and the confirmation email is queued exactly as for a new one. Same account id, so everything it holds stays; the link in the email signs in to it on any device and confirms the address.

{:error, :not_anonymous} for an account that already has an identity.