The Accounts context.
Usage
# Lookup by id or email
user = GameServer.Accounts.get_user(123)
user = GameServer.Accounts.get_user_by_email("me@example.com")
# Update a user
{:ok, user} = GameServer.Accounts.update_user(user, %{display_name: "NewName"})
# Search (paginated) and count
users = GameServer.Accounts.search_users("bob", page: 1, page_size: 25)
count = GameServer.Accounts.count_search_users("bob")Note: This is an SDK stub. Calling these functions will raise an error. The actual implementation runs on the GameServer.
Summary
Functions
Attach a device_id to an existing user record. Returns {:ok, user} or {:error, changeset} if the device_id is already used.
Broadcast a member_updated event to the user's current lobby and
party channels so other members see the profile change (display name, avatar,
metadata, etc.) in real-time.
Broadcast that the given user has been updated.
Returns an %Ecto.Changeset{} for changing the user display_name.
Returns an %Ecto.Changeset{} for changing the user display_name.
Returns an %Ecto.Changeset{} for changing the user email.
Returns an %Ecto.Changeset{} for changing the user email.
Returns an %Ecto.Changeset{} for changing the user email.
Returns an %Ecto.Changeset{} for changing the user password.
Returns an %Ecto.Changeset{} for changing the user password.
Returns an %Ecto.Changeset{} for changing the user password.
Confirms a user's email by setting confirmed_at timestamp.
Confirm a user by an email confirmation token (context: "confirm").
Count users matching a text query (email or display_name). Returns integer.
Count users who are not yet activated (is_activated == false).
Counts tokens for a given user.
Returns the total number of users.
Count users active in the last N days.
Count users currently marked as online.
Count users registered in the last N days.
Count users with a password set (hashed_password not nil/empty).
Count users with non-empty provider id for a given provider field (e.g. :google_id)
Deletes a user and associated resources.
Deletes the signed token with the given context.
Delivers the magic link login instructions to the given user.
Delivers the update email instructions to the given user.
Returns true when device-based auth is enabled. This checks the
application config :game_server, :device_auth_enabled and falls back
to the environment variable DEVICE_AUTH_ENABLED. If neither
is set, device auth is enabled by default.
Finds a user by Apple ID or creates a new user from OAuth data.
Finds or creates a user associated with the given device_id.
Finds or creates a user associated with the given device_id.
Finds a user by Discord ID or creates a new user from OAuth data.
Finds a user by Facebook ID or creates a new user from OAuth data.
Finds a user by Google ID or creates a new user from OAuth data.
Finds a user by Steam ID or creates a new user from Steam OpenID data.
Generates a session token.
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.
Get a user by their Facebook ID.
Get a user by their Google ID.
Gets the user with the given magic link token.
Gets the user with the given signed token.
Get a user by their Steam ID (steam_id).
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.
Link an OAuth provider to an existing user account. Updates the user via the provider's oauth changeset while being careful not to overwrite existing email or avatars.
Link a device_id to an existing user account. This allows the user to authenticate using the device_id in addition to their OAuth providers.
Lists tokens for a given user, optionally filtered by context.
Logs the user in by magic link.
Registers a user.
Register a user and send the confirmation email inside a DB transaction.
Register a user and send the confirmation email inside a DB transaction.
Returns true when new accounts require manual admin activation before
they can log in. Reads from application config
:game_server_core, :require_account_activation which is set at boot
from the REQUIRE_ACCOUNT_ACTIVATION environment variable in runtime.exs.
Defaults to false when not configured.
Revokes all session tokens for a user (mass logout).
Search users by email or display name (case-insensitive, partial match).
Search users by email or display name (case-insensitive, partial match).
Serialize a user into the compact payload used by realtime updates.
Mark a user as offline and update last_seen_at. Returns {:ok, user} on success.
Mark a user as online and update last_seen_at. Returns {:ok, user} on success.
Checks whether the user is in sudo mode.
Checks whether the user is in sudo mode.
Updates last_seen_at to now for the given user. Fire-and-forget — errors are ignored.
Call on login (session or JWT) to track activity.
Lightweight version of touch_last_seen/1 that accepts a user ID directly.
Performs a single UPDATE without loading the full struct first, setting
last_seen_at to now and is_online to true, then invalidates the cache.
Fire-and-forget — errors are ignored.
Unlink the device_id from a user's account.
Unlink an OAuth provider from a user's account.
Updates a user with the given attributes.
Updates the user's display name and broadcasts the change.
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.
Functions
@spec attach_device_to_user(GameServer.Accounts.User.t(), String.t()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Attach a device_id to an existing user record. Returns {:ok, user} or {:error, changeset} if the device_id is already used.
@spec broadcast_member_update(GameServer.Accounts.User.t()) :: :ok
Broadcast a member_updated event to the user's current lobby and
party channels so other members see the profile change (display name, avatar,
metadata, etc.) in real-time.
This is fire-and-forget and safe to call even when the user is not in a lobby or party.
@spec broadcast_user_update(GameServer.Accounts.User.t()) :: :ok
Broadcast that the given user has been updated.
This helper is intentionally small and only broadcasts a compact payload
intended for client consumption through the user:<id> topic.
@spec change_user_display_name(GameServer.Accounts.User.t()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user display_name.
@spec change_user_display_name(GameServer.Accounts.User.t(), map()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user display_name.
@spec change_user_email(GameServer.Accounts.User.t()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user email.
See GameServer.Accounts.User.email_changeset/3 for a list of supported options.
## Examples
iex> change_user_email(user)
%Ecto.Changeset{data: %User{}}
@spec change_user_email(GameServer.Accounts.User.t(), map()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user email.
See GameServer.Accounts.User.email_changeset/3 for a list of supported options.
## Examples
iex> change_user_email(user)
%Ecto.Changeset{data: %User{}}
@spec change_user_email(GameServer.Accounts.User.t(), map(), keyword()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user email.
See GameServer.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(GameServer.Accounts.User.t()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user password.
See GameServer.Accounts.User.password_changeset/3 for a list of supported options.
## Examples
iex> change_user_password(user)
%Ecto.Changeset{data: %User{}}
@spec change_user_password(GameServer.Accounts.User.t(), map()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user password.
See GameServer.Accounts.User.password_changeset/3 for a list of supported options.
## Examples
iex> change_user_password(user)
%Ecto.Changeset{data: %User{}}
@spec change_user_password(GameServer.Accounts.User.t(), map(), keyword()) :: Ecto.Changeset.t()
Returns an %Ecto.Changeset{} for changing the user password.
See GameServer.Accounts.User.password_changeset/3 for a list of supported options.
## Examples
iex> change_user_password(user)
%Ecto.Changeset{data: %User{}}
@spec confirm_user(GameServer.Accounts.User.t()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Confirms a user's email by setting confirmed_at timestamp.
## Examples
iex> confirm_user(user)
{:ok, %User{}}
@spec confirm_user_by_token(String.t()) :: {:ok, GameServer.Accounts.User.t()} | {:error, :invalid | :not_found}
Confirm a user by an email confirmation token (context: "confirm").
Returns {:ok, user} when the token is valid and user was confirmed. Returns {:error, :not_found} or {:error, :expired} when token is invalid/expired.
@spec count_search_users(String.t()) :: non_neg_integer()
Count users matching a text query (email or display_name). Returns integer.
@spec count_unactivated_users() :: non_neg_integer()
Count users who are not yet activated (is_activated == false).
@spec count_user_tokens(integer()) :: non_neg_integer()
Counts tokens for a given user.
@spec count_users() :: non_neg_integer()
Returns the total number of users.
@spec count_users_active_since(integer()) :: non_neg_integer()
Count users active in the last N days.
This metric is based on users.updated_at (any user record update,
including registration/creation), so it reflects all users and not just
session-token based authentication.
@spec count_users_online() :: non_neg_integer()
Count users currently marked as online.
@spec count_users_registered_since(integer()) :: non_neg_integer()
Count users registered in the last N days.
@spec count_users_with_password() :: non_neg_integer()
Count users with a password set (hashed_password not nil/empty).
@spec count_users_with_provider(atom()) :: non_neg_integer()
Count users with non-empty provider id for a given provider field (e.g. :google_id)
@spec delete_user(GameServer.Accounts.User.t()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Deletes a user and associated resources.
Returns {:ok, user} on success or {:error, changeset} on failure.
@spec delete_user_session_token(binary()) :: :ok
Deletes the signed token with the given context.
@spec deliver_login_instructions(GameServer.Accounts.User.t(), (String.t() -> String.t())) :: {:ok, Swoosh.Email.t()} | {:error, term()}
Delivers the magic link login instructions to the given user.
@spec deliver_user_update_email_instructions( GameServer.Accounts.User.t(), String.t(), (String.t() -> String.t()) ) :: {:ok, Swoosh.Email.t()} | {:error, term()}
Delivers the update email instructions to the given user.
## Examples
iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm-email/#{&1}"))
{:ok, %{to: ..., body: ...}}
@spec device_auth_enabled?() :: boolean()
Returns true when device-based auth is enabled. This checks the
application config :game_server, :device_auth_enabled and falls back
to the environment variable DEVICE_AUTH_ENABLED. If neither
is set, device auth is enabled by default.
@spec find_or_create_from_apple(map()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}
Finds a user by Apple ID or creates a new user from OAuth data.
## Examples
iex> find_or_create_from_apple(%{apple_id: "123", email: "user@example.com"})
{:ok, %User{}}
@spec find_or_create_from_device(String.t()) :: {:ok, GameServer.Accounts.User.t()} | {:error, :disabled | Ecto.Changeset.t() | term()}
Finds or creates a user associated with the given device_id.
If a user already exists with the device_id we return it. Otherwise we create an anonymous confirmed user and attach the device_id.
@spec find_or_create_from_device(String.t(), map()) :: {:ok, GameServer.Accounts.User.t()} | {:error, :disabled | Ecto.Changeset.t() | term()}
Finds or creates a user associated with the given device_id.
If a user already exists with the device_id we return it. Otherwise we create an anonymous confirmed user and attach the device_id.
@spec find_or_create_from_discord(map()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}
Finds a user by Discord ID or creates a new user from OAuth data.
## Examples
iex> find_or_create_from_discord(%{discord_id: "123", email: "user@example.com"})
{:ok, %User{}}
@spec find_or_create_from_facebook(map()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}
Finds a user by Facebook ID or creates a new user from OAuth data.
## Examples
iex> find_or_create_from_facebook(%{facebook_id: "123", email: "user@example.com"})
{:ok, %User{}}
@spec find_or_create_from_google(map()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}
Finds a user by Google ID or creates a new user from OAuth data.
## Examples
iex> find_or_create_from_google(%{google_id: "123", email: "user@example.com"})
{:ok, %User{}}
@spec find_or_create_from_steam(map()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}
Finds a user by Steam ID or creates a new user from Steam OpenID data.
## Examples
iex> find_or_create_from_steam(%{steam_id: "12345", email: "user@example.com"})
{:ok, %User{}}
@spec generate_user_session_token(GameServer.Accounts.User.t()) :: binary()
Generates a session token.
@spec get_linked_providers(GameServer.Accounts.User.t()) :: %{ google: boolean(), facebook: 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(integer()) :: GameServer.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(456)
nil
@spec get_user!(integer()) :: GameServer.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()) :: GameServer.Accounts.User.t() | nil
Get a user by their Apple ID.
Returns %User{} or nil.
@spec get_user_by_discord_id(String.t()) :: GameServer.Accounts.User.t() | nil
Get a user by their Discord ID.
Returns %User{} or nil.
@spec get_user_by_email(String.t()) :: GameServer.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()) :: GameServer.Accounts.User.t() | nil
Gets a user by email and password.
## 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()) :: GameServer.Accounts.User.t() | nil
Get a user by their Facebook ID.
Returns %User{} or nil.
@spec get_user_by_google_id(String.t()) :: GameServer.Accounts.User.t() | nil
Get a user by their Google ID.
Returns %User{} or nil.
@spec get_user_by_magic_link_token(String.t()) :: GameServer.Accounts.User.t() | nil
Gets the user with the given magic link token.
@spec get_user_by_session_token(binary()) :: {GameServer.Accounts.User.t(), DateTime.t()} | nil
Gets the user with the given signed token.
If the token is valid {user, token_inserted_at} is returned, otherwise nil is returned.
@spec get_user_by_steam_id(String.t()) :: GameServer.Accounts.User.t() | nil
Get a user by their Steam ID (steam_id).
Returns %User{} or nil.
@spec has_password?(GameServer.Accounts.User.t()) :: boolean()
Returns whether the user has a password set.
@spec invalidate_user_cache_by_id(integer()) :: :ok
Public cache invalidation for cross-module use (lobbies, parties, groups). Accepts a user ID and clears both the primary and all index caches.
@spec link_account( GameServer.Accounts.User.t(), map(), atom(), (GameServer.Accounts.User.t(), map() -> Ecto.Changeset.t()) ) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | {:conflict, GameServer.Accounts.User.t()}}
Link an OAuth provider to an existing user account. Updates the user via the provider's oauth changeset while being careful not to overwrite existing email or avatars.
Example: link_account(user, %{discord_id: "123", profile_url: "https://..."}, :discord_id, &User.discord_oauth_changeset/2)
@spec link_device_id(GameServer.Accounts.User.t(), String.t()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Link a device_id to an existing user account. This allows the user to authenticate using the device_id in addition to their OAuth providers.
Returns {:ok, user} on success or {:error, changeset} if the device_id is already used by another account.
Lists tokens for a given user, optionally filtered by context.
@spec login_user_by_magic_link(String.t()) :: {:ok, {GameServer.Accounts.User.t(), [GameServer.Accounts.UserToken.t()]}} | {:error, :not_found | Ecto.Changeset.t() | term()}
Logs the user in by magic link.
There are three cases to consider:
The user has already confirmed their email. They are logged in and the magic link is expired.
The user has not confirmed their email and no password is set. In this case, the user gets confirmed, logged in, and all tokens - including session ones - are expired. In theory, no other tokens exist but we delete all of them for best security practices.
The user has not confirmed their email but a password is set. This cannot happen in the default implementation but may be the source of security pitfalls. See the "Mixing magic link and password registration" section of
mix help phx.gen.auth.
@spec register_user(GameServer.Types.user_registration_attrs()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Registers a user.
## Attributes
See GameServer.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{}}
@spec register_user_and_deliver( GameServer.Types.user_registration_attrs(), (String.t() -> String.t()) ) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}
Register a user and send the confirmation email inside a DB transaction.
The function accepts a confirmation_url_fun which must be a function of arity 1
that receives the encoded token and returns the confirmation URL string.
If sending the confirmation email fails the transaction is rolled back and
{:error, reason} is returned. On success it returns {:ok, user}.
@spec register_user_and_deliver( GameServer.Types.user_registration_attrs(), (String.t() -> String.t()), module() ) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t() | term()}
Register a user and send the confirmation email inside a DB transaction.
The function accepts a confirmation_url_fun which must be a function of arity 1
that receives the encoded token and returns the confirmation URL string.
If sending the confirmation email fails the transaction is rolled back and
{:error, reason} is returned. On success it returns {:ok, user}.
@spec require_account_activation?() :: boolean()
Returns true when new accounts require manual admin activation before
they can log in. Reads from application config
:game_server_core, :require_account_activation which is set at boot
from the REQUIRE_ACCOUNT_ACTIVATION environment variable in runtime.exs.
Defaults to false when not configured.
@spec revoke_all_user_sessions(integer()) :: {non_neg_integer(), nil}
Revokes all session tokens for a user (mass logout).
@spec search_users(String.t()) :: [GameServer.Accounts.User.t()]
Search users by email or display name (case-insensitive, partial match).
Returns a list of User structs.
## Options
See GameServer.Types.pagination_opts/0 for available options.
@spec search_users(String.t(), GameServer.Types.pagination_opts()) :: [ GameServer.Accounts.User.t() ]
Search users by email or display name (case-insensitive, partial match).
Returns a list of User structs.
## Options
See GameServer.Types.pagination_opts/0 for available options.
@spec serialize_user_payload(GameServer.Accounts.User.t()) :: map()
Serialize a user into the compact payload used by realtime updates.
@spec set_user_offline(GameServer.Accounts.User.t() | integer()) :: {:ok, GameServer.Accounts.User.t()} | {:error, term()}
Mark a user as offline and update last_seen_at. Returns {:ok, user} on success.
@spec set_user_online(GameServer.Accounts.User.t() | integer()) :: {:ok, GameServer.Accounts.User.t()} | {:error, term()}
Mark a user as online and update last_seen_at. Returns {:ok, user} on success.
@spec sudo_mode?(GameServer.Accounts.User.t()) :: boolean()
Checks whether the user is in sudo mode.
The user is in sudo mode when the last authentication was done no further than 20 minutes ago. The limit can be given as second argument in minutes.
@spec sudo_mode?(GameServer.Accounts.User.t(), integer()) :: boolean()
Checks whether the user is in sudo mode.
The user is in sudo mode when the last authentication was done no further than 20 minutes ago. The limit can be given as second argument in minutes.
@spec touch_last_seen(GameServer.Accounts.User.t()) :: :ok
Updates last_seen_at to now for the given user. Fire-and-forget — errors are ignored.
Call on login (session or JWT) to track activity.
@spec touch_last_seen_by_id(integer()) :: :ok
Lightweight version of touch_last_seen/1 that accepts a user ID directly.
Performs a single UPDATE without loading the full struct first, setting
last_seen_at to now and is_online to true, then invalidates the cache.
Fire-and-forget — errors are ignored.
@spec unlink_device_id(GameServer.Accounts.User.t()) :: {:ok, GameServer.Accounts.User.t()} | {:error, :last_auth_method | Ecto.Changeset.t()}
Unlink the device_id from a user's account.
Returns {:ok, user} when successful or {:error, reason}.
Guard: we only allow unlinking when the user will still have at least one authentication method remaining (OAuth provider or password). This prevents users losing all login methods unexpectedly.
@spec unlink_provider( GameServer.Accounts.User.t(), :discord | :apple | :google | :facebook | :steam ) :: {:ok, GameServer.Accounts.User.t()} | {:error, :last_provider | Ecto.Changeset.t() | term()}
Unlink an OAuth provider from a user's account.
provider should be one of :discord, :apple, :google, :facebook. This will return {:ok, user} when successful or {:error, reason}.
Guard: we only allow unlinking when the user will still have at least one other social provider remaining. This prevents users losing all social logins unexpectedly.
@spec update_user(GameServer.Accounts.User.t(), GameServer.Types.user_update_attrs()) :: {:ok, GameServer.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 GameServer.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_display_name(GameServer.Accounts.User.t(), map()) :: {:ok, GameServer.Accounts.User.t()} | {:error, Ecto.Changeset.t()}
Updates the user's display name and broadcasts the change.
@spec update_user_email(GameServer.Accounts.User.t(), String.t()) :: {:ok, GameServer.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(GameServer.Accounts.User.t(), map()) :: {:ok, {GameServer.Accounts.User.t(), [GameServer.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{}}
@spec user_activated?(GameServer.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.