Gamend.Accounts (gamend_core v1.0.1296)

Copy Markdown View Source

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

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

password_error()

@type password_error() ::
  :invalid_credentials | :email_not_confirmed | {:locked, pos_integer()}

Why authenticate_by_password/2 signed nobody in.

Functions

attach_device_to_user(user, device_id)

See Gamend.Accounts.Identities.attach_device_to_user/2.

authenticate_by_password(email, password)

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

broadcast_friend_update(user)

See Gamend.Accounts.Broadcasts.broadcast_friend_update/1.

broadcast_member_update(user)

See Gamend.Accounts.Broadcasts.broadcast_member_update/1.

broadcast_user_update(user)

See Gamend.Accounts.Broadcasts.broadcast_user_update/1.

cache_user(user)

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.

can_upload_avatar?(user)

@spec can_upload_avatar?(Gamend.Accounts.User.t()) :: boolean()

Whether user may upload an avatar, per anonymous_can_upload_avatar.

cancel_deletion(user)

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

change_user_display_name(user, attrs \\ %{})

See Gamend.Accounts.Profile.change_user_display_name/2.

change_user_email(user, attrs \\ %{}, opts \\ [])

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

change_user_password(user, attrs \\ %{}, opts \\ [])

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

change_user_registration(user, attrs \\ %{})

See Gamend.Accounts.Registration.change_user_registration/2.

change_user_registration_for_validation(user, attrs)

See Gamend.Accounts.Registration.change_user_registration_for_validation/2.

change_username(user, attrs \\ %{})

See Gamend.Accounts.Profile.change_username/2.

confirm_user(user)

See Gamend.Accounts.Registration.confirm_user/1.

confirm_user_by_code(email, code, password)

See Gamend.Accounts.Registration.confirm_user_by_code/3.

confirm_user_by_token(token)

See Gamend.Accounts.Registration.confirm_user_by_token/1.

count_admins()

See Gamend.Accounts.Stats.count_admins/0.

count_list_all_users(filters \\ %{})

See Gamend.Accounts.Search.count_list_all_users/1.

count_search_users(query)

See Gamend.Accounts.Search.count_search_users/1.

count_unactivated_users()

See Gamend.Accounts.Stats.count_unactivated_users/0.

count_user_tokens(user_id)

See Gamend.Accounts.Sessions.count_user_tokens/1.

count_users()

See Gamend.Accounts.Stats.count_users/0.

count_users_in_lobbies()

See Gamend.Accounts.Stats.count_users_in_lobbies/0.

count_users_in_parties()

See Gamend.Accounts.Stats.count_users_in_parties/0.

count_users_online()

See Gamend.Accounts.Stats.count_users_online/0.

count_users_with_password()

See Gamend.Accounts.Stats.count_users_with_password/0.

count_users_with_provider(provider_field)

See Gamend.Accounts.Stats.count_users_with_provider/1.

delete_user(user)

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

delete_user_session_token(token)

See Gamend.Accounts.Sessions.delete_user_session_token/1.

delete_user_storage(user_id)

See Gamend.Accounts.Profile.delete_user_storage/1.

deletion_grace_days()

@spec deletion_grace_days() :: non_neg_integer()

Days a player's own deletion waits (auth.deletion_grace_days); 0 deletes at once.

deletion_scheduled?(user)

@spec deletion_scheduled?(Gamend.Accounts.User.t() | nil) :: boolean()

Whether user is waiting out a deletion grace period.

deliver_login_instructions(user, magic_link_url_fun)

See Gamend.Accounts.Sessions.deliver_login_instructions/2.

deliver_user_confirmation_instructions(user, confirmation_url_fun)

See Gamend.Accounts.Registration.deliver_user_confirmation_instructions/2.

deliver_user_update_email_instructions(user, current_email, update_email_url_fun)

See Gamend.Accounts.Sessions.deliver_user_update_email_instructions/3.

device_auth_enabled?()

@spec device_auth_enabled?() :: boolean()

Whether device-based auth is enabled. Defaults to on.

display_label(user)

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

display_name(user)

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

due_deletions_query()

@spec due_deletions_query() :: Ecto.Query.t()

Accounts whose deletion date has passed. For Gamend.Retention.

find_or_create_from_apple(attrs)

See Gamend.Accounts.Identities.find_or_create_from_apple/1.

find_or_create_from_device(device_id, attrs \\ %{})

See Gamend.Accounts.Identities.find_or_create_from_device/2.

find_or_create_from_discord(attrs)

See Gamend.Accounts.Identities.find_or_create_from_discord/1.

find_or_create_from_facebook(attrs)

See Gamend.Accounts.Identities.find_or_create_from_facebook/1.

find_or_create_from_github(attrs)

See Gamend.Accounts.Identities.find_or_create_from_github/1.

find_or_create_from_google(attrs)

See Gamend.Accounts.Identities.find_or_create_from_google/1.

find_or_create_from_steam(attrs)

See Gamend.Accounts.Identities.find_or_create_from_steam/1.

generate_user_session_token(user)

See Gamend.Accounts.Sessions.generate_user_session_token/1.

get_linked_providers(user)

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

get_user(id)

@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

get_user!(id)

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

get_user_by_apple_id(apple_id)

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

Get a user by their Apple ID.

Returns %User{} or nil.

get_user_by_confirm_token(token)

See Gamend.Accounts.Registration.get_user_by_confirm_token/1.

get_user_by_discord_id(discord_id)

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

Get a user by their Discord ID.

Returns %User{} or nil.

get_user_by_email(email)

@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

get_user_by_email_and_password(email, password)

@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

get_user_by_facebook_id(facebook_id)

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

Get a user by their Facebook ID.

Returns %User{} or nil.

get_user_by_github_id(github_id)

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

Get a user by their GitHub ID.

Returns %User{} or nil.

get_user_by_google_id(google_id)

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

Get a user by their Google ID.

Returns %User{} or nil.

get_user_by_session_token(token)

See Gamend.Accounts.Sessions.get_user_by_session_token/1.

get_user_by_steam_id(steam_id)

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

get_user_by_username(username)

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

has_password?(user)

@spec has_password?(Gamend.Accounts.User.t()) :: boolean()

Returns whether the user has a password set.

invalidate_user_cache_by_id(user_id)

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

list_admin_ids()

See Gamend.Accounts.Stats.list_admin_ids/0.

list_all_users(filters \\ %{}, opts \\ [])

See Gamend.Accounts.Search.list_all_users/2.

list_user_tokens(user_id, opts \\ [])

See Gamend.Accounts.Sessions.list_user_tokens/2.

login_user_by_magic_link(token)

See Gamend.Accounts.Sessions.login_user_by_magic_link/1.

merge_metadata(user, patch)

See Gamend.Accounts.Profile.merge_metadata/2.

player_stats()

See Gamend.Accounts.Stats.player_stats/0.

prune_user_avatars(user_id, keep_key)

See Gamend.Accounts.Profile.prune_user_avatars/2.

refresh_account_class(user)

See Gamend.Accounts.Profile.refresh_account_class/1.

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

See Gamend.Accounts.Registration.register_unconfirmed_user_and_deliver/3.

register_user(attrs)

See Gamend.Accounts.Registration.register_user/1.

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

See Gamend.Accounts.Registration.register_user_and_deliver/3.

request_deletion(user)

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

require_account_activation?()

@spec require_account_activation?() :: boolean()

Whether new accounts require manual admin activation before they can log in.

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

See Gamend.Accounts.Registration.resend_confirmation/3.

revoke_all_tokens(user)

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

revoke_all_user_sessions(user_id)

See Gamend.Accounts.Sessions.revoke_all_user_sessions/1.

search_users(query, opts \\ [])

See Gamend.Accounts.Search.search_users/2.

serialize_user_payload(user)

See Gamend.Accounts.Broadcasts.serialize_user_payload/1.

set_user_age(user, attrs)

See Gamend.Accounts.Profile.set_user_age/2.

set_user_offline(user_id)

See Gamend.Accounts.Presence.set_user_offline/1.

set_user_online(user_id)

See Gamend.Accounts.Presence.set_user_online/1.

sudo_mode?(user)

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

sudo_mode?(user, minutes)

@spec sudo_mode?(Gamend.Accounts.User.t(), integer()) :: boolean()

sudo_mode_minutes()

@spec sudo_mode_minutes() :: pos_integer()

How recently a user must have signed in to open a sudo page (auth.sudo_mode_minutes).

touch_last_seen(user)

See Gamend.Accounts.Presence.touch_last_seen/1.

touch_last_seen_by_id(user_id)

See Gamend.Accounts.Presence.touch_last_seen_by_id/1.

update_user(user, attrs)

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{}}

update_user_avatar(user, url)

See Gamend.Accounts.Profile.update_user_avatar/2.

update_user_display_name(user, attrs)

See Gamend.Accounts.Profile.update_user_display_name/2.

update_user_email(user, token)

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

update_user_password(user, attrs)

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

update_username(user, attrs)

See Gamend.Accounts.Profile.update_username/2.

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

See Gamend.Accounts.Registration.upgrade_anonymous_user_and_deliver/4.

user_activated?(arg1)

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

user_exists?(id)

@spec user_exists?(term()) :: boolean()

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.

users_by_ids(ids)

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

valid_password?(user, password)

@spec valid_password?(Gamend.Accounts.User.t(), term()) :: boolean()

Returns true when password matches the user's current password.