The Accounts context.
Usage
# Lookup by id or email
user = Gamend.Accounts.get_user(123)
user = Gamend.Accounts.get_user_by_email("me@example.com")
# Update a user
{:ok, user} = Gamend.Accounts.update_user(user, %{display_name: "NewName"})
# Search (paginated) and count
users = Gamend.Accounts.search_users("bob", page: 1, page_size: 25)
count = Gamend.Accounts.count_search_users("bob")
Summary
Types
Why authenticate_by_password/2 signed nobody in.
Functions
Checks an email and password, counting failures per address
(Gamend.Accounts.LoginLockouts).
Stores user under the canonical user cache key (with the standard TTL).
Whether user may upload an avatar, per anonymous_can_upload_avatar.
Keep an account that was scheduled for deletion. A no-op for one that was not.
Returns an %Ecto.Changeset{} for changing the user email.
Returns an %Ecto.Changeset{} for changing the user password.
Deletes a user and associated resources.
Days a player's own deletion waits (auth.deletion_grace_days); 0 deletes at once.
Whether user is waiting out a deletion grace period.
Whether device-based auth is enabled. Defaults to on.
How to name a user in text a PLAYER reads: "Ana (drift-2378)", or just the
username when there is no display name. Mirrors the client's
UserDisplayUtil.name_with_username, so a notification and the friends list
it sends you to name the same person the same way.
The short form of display_label/1: the display name, or the username when
there is none. No parenthesised handle.
Accounts whose deletion date has passed. For Gamend.Retention.
Returns a map of linked OAuth providers for the user.
Gets a single user by ID.
Gets a single user.
Get a user by their Apple ID.
Get a user by their Discord ID.
Gets a user by email.
Gets a user by email and password. nil for a wrong password, for an
address locked by too many failures, and for an email not yet confirmed
(authenticate_by_password/2 says which).
Get a user by their Facebook ID.
Get a user by their GitHub ID.
Get a user by their Google ID.
Get a user by their Steam ID (steam_id).
Gets a user by their unique username handle (case-insensitive; usernames are stored lowercase).
Returns whether the user has a password set.
Public cache invalidation for cross-module use (lobbies, parties, groups). Accepts a user ID and clears both the primary and all index caches.
A player deleting their own account.
Whether new accounts require manual admin activation before they can log in.
Revokes every credential the user holds: all session tokens are deleted and
token_version is bumped, which invalidates all previously issued JWT
access and refresh tokens ("log out everywhere").
Checks whether the user is in sudo mode.
How recently a user must have signed in to open a sudo page (auth.sudo_mode_minutes).
Updates a user with the given attributes.
Updates the user email using the given token.
Updates the user password.
Returns true when the given user is activated or when account activation
is not required. Returns false only when activation is required and
the user's is_activated flag is false.
Whether an account with this id exists.
Map of %{id => %User{}} for the given ids, for batch name lookups (e.g. admin
tables that hold only a user_id). Nil/duplicate ids are ignored.
Returns true when password matches the user's current password.
Types
@type password_error() :: :invalid_credentials | :email_not_confirmed | {:locked, pos_integer()}
Why authenticate_by_password/2 signed nobody in.
Functions
@spec authenticate_by_password(String.t(), String.t()) :: {:ok, Gamend.Accounts.User.t()} | {:error, password_error()}
Checks an email and password, counting failures per address
(Gamend.Accounts.LoginLockouts).
{:error, {:locked, seconds}} when the address is locked, before the
password is looked at, and for the failure that locks it.
{:error, :email_not_confirmed} for the right password on an account whose
email was never confirmed: registering takes no password, so only a guest
account given an email, or one registered when the API still took one, can
have it. The password signs nobody in until the inbox's owner has confirmed
it. It is answered only after the password matched, so it tells nothing to
someone who does not know it.
@spec cache_user(Gamend.Accounts.User.t()) :: Gamend.Accounts.User.t()
Stores user under the canonical user cache key (with the standard TTL).
Call after writes that update the user row outside this module (e.g. lobby
or party membership) so subsequent get_user/1 reads stay warm and
consistent instead of serving the pre-write struct until the TTL expires.
@spec can_upload_avatar?(Gamend.Accounts.User.t()) :: boolean()
Whether user may upload an avatar, per anonymous_can_upload_avatar.
@spec cancel_deletion(Gamend.Accounts.User.t()) :: {:ok, Gamend.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Keep an account that was scheduled for deletion. A no-op for one that was not.
@spec change_user_email(Gamend.Accounts.User.t(), map(), keyword()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user email.
See Gamend.Accounts.User.email_changeset/3 for a list of supported options.
Examples
iex> change_user_email(user)
%Ecto.Changeset{data: %User{}}
@spec change_user_password(Gamend.Accounts.User.t(), map(), keyword()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user password.
See Gamend.Accounts.User.password_changeset/3 for a list of supported options.
Examples
iex> change_user_password(user)
%Ecto.Changeset{data: %User{}}
See Gamend.Accounts.Registration.change_user_registration/2.
See Gamend.Accounts.Registration.change_user_registration_for_validation/2.
@spec delete_user(Gamend.Accounts.User.t()) :: {:ok, Gamend.Accounts.User.t()} | {:error, :not_found | Ecto.Changeset.t()}
Deletes a user and associated resources.
Returns {:ok, user} on success, {:error, :not_found} when the user was
deleted first, or {:error, changeset} on failure.
@spec deletion_grace_days() :: non_neg_integer()
Days a player's own deletion waits (auth.deletion_grace_days); 0 deletes at once.
@spec deletion_scheduled?(Gamend.Accounts.User.t() | nil) :: boolean()
Whether user is waiting out a deletion grace period.
See Gamend.Accounts.Registration.deliver_user_confirmation_instructions/2.
See Gamend.Accounts.Sessions.deliver_user_update_email_instructions/3.
@spec device_auth_enabled?() :: boolean()
Whether device-based auth is enabled. Defaults to on.
@spec display_label(Gamend.Accounts.User.t() | Ecto.UUID.t() | nil) :: String.t()
How to name a user in text a PLAYER reads: "Ana (drift-2378)", or just the
username when there is no display name. Mirrors the client's
UserDisplayUtil.name_with_username, so a notification and the friends list
it sends you to name the same person the same way.
Never falls back to the id. Every account has a server-assigned username, and
"User #0198f7be-…" reads like a name while telling the reader nothing.
@spec display_name(Gamend.Accounts.User.t() | Ecto.UUID.t() | nil) :: String.t()
The short form of display_label/1: the display name, or the username when
there is none. No parenthesised handle.
Use this where the name sits inside a sentence the player reads — "Ana
invited you" — and display_label/1 where it stands on its own and the
handle disambiguates, such as an admin table or a friends list.
This exists because the fallback was being written inline, differently, in
four places: parties sent display_name || "", so an invite from a player
who had set no display name arrived from nobody; group invites wrote
display_name || username; three admin views fell through to the email and
then the raw id, which display_label/1 documents as the thing not to do.
@spec due_deletions_query() :: Ecto.Query.t()
Accounts whose deletion date has passed. For Gamend.Retention.
See Gamend.Accounts.Identities.find_or_create_from_device/2.
See Gamend.Accounts.Identities.find_or_create_from_discord/1.
See Gamend.Accounts.Identities.find_or_create_from_facebook/1.
See Gamend.Accounts.Identities.find_or_create_from_github/1.
See Gamend.Accounts.Identities.find_or_create_from_google/1.
@spec get_linked_providers(Gamend.Accounts.User.t()) :: %{ google: boolean(), facebook: boolean(), github: boolean(), discord: boolean(), apple: boolean(), steam: boolean(), device: boolean() }
Returns a map of linked OAuth providers for the user.
Each provider is a boolean indicating whether that provider is linked.
@spec get_user(Ecto.UUID.t()) :: Gamend.Accounts.User.t() | nil
Gets a single user by ID.
Returns nil if the User does not exist.
Examples
iex> get_user(123)
%User{}
iex> get_user(Ecto.UUID.generate())
nil
@spec get_user!(Ecto.UUID.t()) :: Gamend.Accounts.User.t()
Gets a single user.
Raises Ecto.NoResultsError if the User does not exist.
Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
@spec get_user_by_apple_id(String.t()) :: Gamend.Accounts.User.t() | nil
Get a user by their Apple ID.
Returns %User{} or nil.
See Gamend.Accounts.Registration.get_user_by_confirm_token/1.
@spec get_user_by_discord_id(String.t()) :: Gamend.Accounts.User.t() | nil
Get a user by their Discord ID.
Returns %User{} or nil.
@spec get_user_by_email(String.t()) :: Gamend.Accounts.User.t() | nil
Gets a user by email.
Examples
iex> get_user_by_email("foo@example.com")
%User{}
iex> get_user_by_email("unknown@example.com")
nil
@spec get_user_by_email_and_password(String.t(), String.t()) :: Gamend.Accounts.User.t() | nil
Gets a user by email and password. nil for a wrong password, for an
address locked by too many failures, and for an email not yet confirmed
(authenticate_by_password/2 says which).
Examples
iex> get_user_by_email_and_password("foo@example.com", "correct_password")
%User{}
iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
nil
@spec get_user_by_facebook_id(String.t()) :: Gamend.Accounts.User.t() | nil
Get a user by their Facebook ID.
Returns %User{} or nil.
@spec get_user_by_github_id(String.t()) :: Gamend.Accounts.User.t() | nil
Get a user by their GitHub ID.
Returns %User{} or nil.
@spec get_user_by_google_id(String.t()) :: Gamend.Accounts.User.t() | nil
Get a user by their Google ID.
Returns %User{} or nil.
See Gamend.Accounts.Sessions.get_user_by_magic_link_token/1.
@spec get_user_by_steam_id(String.t()) :: Gamend.Accounts.User.t() | nil
Get a user by their Steam ID (steam_id).
Returns %User{} or nil.
@spec get_user_by_username(String.t()) :: Gamend.Accounts.User.t() | nil
Gets a user by their unique username handle (case-insensitive; usernames are stored lowercase).
@spec has_password?(Gamend.Accounts.User.t()) :: boolean()
Returns whether the user has a password set.
@spec invalidate_user_cache_by_id(Ecto.UUID.t()) :: :ok
Public cache invalidation for cross-module use (lobbies, parties, groups). Accepts a user ID and clears both the primary and all index caches.
See Gamend.Accounts.Registration.register_unconfirmed_user_and_deliver/3.
See Gamend.Accounts.Registration.register_user_and_deliver/3.
@spec request_deletion(Gamend.Accounts.User.t()) :: {:ok, :deleted} | {:ok, {:scheduled, Gamend.Accounts.User.t(), [Gamend.Accounts.UserToken.t()]}} | {:error, Ecto.Changeset.t()}
A player deleting their own account.
With auth.deletion_grace_days at 0 the account is deleted now, by
delete_user/1. Otherwise it is scheduled that many days out and signed out
everywhere (every session, access, refresh and personal API token), and
Gamend.Retention deletes it on the day unless its owner signs in on the
website first (cancel_deletion/1). An account already scheduled keeps its
date. The expired session tokens come back so the caller can disconnect
their LiveViews.
Admin deletions and the retention sweeps call delete_user/1 and never wait.
@spec require_account_activation?() :: boolean()
Whether new accounts require manual admin activation before they can log in.
@spec revoke_all_tokens(Gamend.Accounts.User.t()) :: {:ok, {Gamend.Accounts.User.t(), [Gamend.Accounts.UserToken.t()]}} | {:error, Ecto.Changeset.t()}
Revokes every credential the user holds: all session tokens are deleted and
token_version is bumped, which invalidates all previously issued JWT
access and refresh tokens ("log out everywhere").
Returns {:ok, {user, expired_tokens}}.
@spec sudo_mode?(Gamend.Accounts.User.t()) :: boolean()
Checks whether the user is in sudo mode.
With one argument, the window is the one a sudo form is submitted in:
sudo_mode_minutes/0 plus ten minutes to fill the form in. The limit can be
given as second argument in minutes (negative, as an offset from now).
@spec sudo_mode?(Gamend.Accounts.User.t(), integer()) :: boolean()
@spec sudo_mode_minutes() :: pos_integer()
How recently a user must have signed in to open a sudo page (auth.sudo_mode_minutes).
@spec update_user(Gamend.Accounts.User.t(), Gamend.Types.user_update_attrs()) :: {:ok, Gamend.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Updates a user with the given attributes.
This function applies the User.admin_changeset/2 then updates the user and
broadcasts the update on success. It returns the same tuple shape as
Repo.update/1 so callers can pattern-match as before.
Attributes
See Gamend.Types.user_update_attrs/0 for available fields.
Examples
iex> update_user(user, %{display_name: "NewName"})
{:ok, %User{}}
iex> update_user(user, %{metadata: %{level: 5}})
{:ok, %User{}}
@spec update_user_email(Gamend.Accounts.User.t(), String.t()) :: {:ok, Gamend.Accounts.User.t()} | {:error, :transaction_aborted}
Updates the user email using the given token.
If the token matches, the user email is updated and the token is deleted.
@spec update_user_password(Gamend.Accounts.User.t(), map()) :: {:ok, {Gamend.Accounts.User.t(), [Gamend.Accounts.UserToken.t()]}} | {:error, Ecto.Changeset.t()}
Updates the user password.
Returns a tuple with the updated user, as well as a list of expired tokens.
Examples
iex> update_user_password(user, %{password: ...})
{:ok, {%User{}, [...]}}
iex> update_user_password(user, %{password: "too short"})
{:error, %Ecto.Changeset{}}
See Gamend.Accounts.Registration.upgrade_anonymous_user_and_deliver/4.
@spec user_activated?(Gamend.Accounts.User.t()) :: boolean()
Returns true when the given user is activated or when account activation
is not required. Returns false only when activation is required and
the user's is_activated flag is false.
Whether an account with this id exists.
For the contexts that write a row pointing at a user, before they write it.
SQLite — the default adapter — does not report which constraint an
INSERT violated, only that one was violated, so
Ecto.Changeset.foreign_key_constraint/2 cannot match it and Ecto raises
Ecto.ConstraintError instead of returning a changeset. The declarations in
those schemas are therefore decorative on SQLite (the adapter's own docs say
so), and a bad user_id reaching the database surfaced as a 500.
Checking first costs one indexed read and gives the caller a real answer. It is not a substitute for the foreign key: the row can still be deleted between this and the write. That race ends where it did before, which is why the constraint stays declared.
Returns false for a malformed id rather than raising, since these ids come
from request bodies and hook arguments.
Goes through get_user/1 rather than a bare Repo.exists?, because this sits
on the hot write paths — a currency grant, a score submission — and
get_user/1 is cached. A player earning currency during a session was
authenticated moments ago, so their row is already in the cache and this costs
nothing; Repo.exists? would take a connection from the pool every time.
Correctness is unchanged: delete_user/1 invalidates that entry, and a
nil lookup is never cached (cache_match/1), so a missing user is re-checked
against the database each time rather than being remembered as absent.
@spec users_by_ids([Ecto.UUID.t()]) :: %{ required(Ecto.UUID.t()) => Gamend.Accounts.User.t() }
Map of %{id => %User{}} for the given ids, for batch name lookups (e.g. admin
tables that hold only a user_id). Nil/duplicate ids are ignored.
@spec valid_password?(Gamend.Accounts.User.t(), term()) :: boolean()
Returns true when password matches the user's current password.