# `PhoenixKit.Users.Auth`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.15.1/lib/phoenix_kit/users/auth.ex#L1)

The Auth context for user authentication and management.

This module provides functions for user registration, authentication, password management,
and email confirmation. It serves as the main interface for all user-related operations
in PhoenixKit.

## Core Functions

### User Registration and Authentication

- `register_user/1` - Register a new user with email and password
- `get_user_by_email_and_password/2` - Authenticate user credentials
- `get_user_by_email/1` - Find user by email address

### Password Management

- `change_user_password/2` - Update user password
- `reset_user_password/2` - Reset password with token
- `deliver_user_reset_password_instructions/1` - Send password reset email

### Email Confirmation

- `deliver_user_confirmation_instructions/1` - Send confirmation email
- `confirm_user/1` - Confirm user account with token
- `update_user_email/2` - Change user email with confirmation

### Session Management

- `generate_user_session_token/1` - Create session token for login
- `get_user_by_session_token/1` - Get user from session token
- `delete_user_session_token/1` - Logout user session

## Usage Examples

    # Register a new user
    {:ok, user} = PhoenixKit.Users.Auth.register_user(%{
      email: "user@example.com",
      password: "secure_password123"
    })

    # Authenticate user
    case PhoenixKit.Users.Auth.get_user_by_email_and_password(email, password) do
      {:ok, user} -> {:ok, user}
      {:error, :invalid_credentials} -> {:error, :invalid_credentials}
      {:error, :rate_limit_exceeded} -> {:error, :rate_limit_exceeded}
    end

    # Send confirmation email
    PhoenixKit.Users.Auth.deliver_user_confirmation_instructions(user)

## Security Features

- Passwords are hashed using bcrypt
- Email confirmation prevents unauthorized account creation
- Session tokens provide secure authentication
- Password reset tokens expire for security
- All sensitive operations are logged

# `admin_confirm_user`

Manually confirms a user account (admin function).

## Examples

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

    iex> admin_confirm_user(invalid_user)
    {:error, %Ecto.Changeset{}}

# `admin_unconfirm_user`

Manually unconfirms a user account (admin function).

## Examples

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

    iex> admin_unconfirm_user(invalid_user)
    {:error, %Ecto.Changeset{}}

# `admin_update_user_password`

Updates the user password as an admin (bypasses current password validation).

## Parameters
  * `user` - The user whose password is being updated
  * `attrs` - Password attributes (password, password_confirmation)
  * `context` - Optional context map containing:
    * `:admin_user` - The admin performing the action. **Authorizes as well as
      audits**: when present, the write is refused unless
      `can_manage_user_credentials?/2` allows this actor over this target.
    * `:ip_address` - IP address of the admin (for audit logging)
    * `:user_agent` - User agent of the admin (for audit logging)

Omitting `:admin_user` is the system path — seeds, migrations and mix tasks
act with no actor and are not rank-checked, matching `update_user_status/3`.
A web caller must always pass it; without it there is nothing to check.

## Examples

    iex> admin_update_user_password(user, %{password: "new_password", password_confirmation: "new_password"})
    {:ok, %User{}}

    iex> admin_update_user_password(user, %{password: "new_password", password_confirmation: "new_password"}, %{admin_user: admin, ip_address: "192.168.1.1"})
    {:ok, %User{}}

    iex> admin_update_user_password(owner, %{password: "new_password"}, %{admin_user: admin})
    {:error, :insufficient_permissions}

    iex> admin_update_user_password(user, %{password: "short"})
    {:error, %Ecto.Changeset{}}

# `apply_user_email`

Emulates that the email will change without actually changing
it in the database.

## Examples

    iex> apply_user_email(user, "valid password", %{email: ...})
    {:ok, %User{}}

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

# `assign_role`

Assigns a role to a user.

## Examples

    iex> assign_role(user, "Admin")
    {:ok, %RoleAssignment{}}

    iex> assign_role(user, "Admin", assigned_by_user)
    {:ok, %RoleAssignment{}}

    iex> assign_role(user, "NonexistentRole")
    {:error, :role_not_found}

# `assign_roles_to_existing_users`

Assigns roles to existing users who don't have any PhoenixKit roles.

This is useful for migration scenarios where PhoenixKit is installed
into an existing application with users.

## Examples

    iex> assign_roles_to_existing_users()
    {:ok, %{assigned_owner: 1, assigned_users: 5, total_processed: 6}}

# `bulk_update_user_fields`

Bulk update multiple users with the same field values.

This function updates multiple users at once with the same set of fields.
Each user is updated independently, and the function returns a list of results
showing which updates succeeded and which failed.

Both schema fields and custom fields can be updated in the same call.

## Parameters
- `users` - List of User structs to update
- `attrs` - Map of field names to values (can include both schema and custom fields)

## Returns
Returns `{:ok, results}` where results is a list of tuples:
- `{:ok, user}` - Successfully updated user
- `{:error, changeset}` - Failed update with error details

## Examples

    # Update multiple users with the same fields
    iex> users = [user1, user2, user3]
    iex> bulk_update_user_fields(users, %{status: "active", department: "Engineering"})
    {:ok, [
      {:ok, %User{status: "active", custom_fields: %{"department" => "Engineering"}}},
      {:ok, %User{status: "active", custom_fields: %{"department" => "Engineering"}}},
      {:error, %Ecto.Changeset{}}
    ]}

    # Update both schema and custom fields
    iex> bulk_update_user_fields(users, %{
    ...>   first_name: "John",           # Schema field
    ...>   last_name: "Doe",             # Schema field
    ...>   custom_field_1: "value1",     # Custom field
    ...>   custom_field_2: "value2"      # Custom field
    ...> })
    {:ok, [results...]}

# `calculate_file_hash`

Calculate SHA256 hash of a file.

Used internally for file integrity verification.

## Parameters
- `file_path` - Path to the file

## Returns
- String containing the lowercase hexadecimal SHA256 hash

# `can_delete_user?`

Checks if a user can be deleted by the current user.

Returns `:ok` if deletion is allowed, or `{:error, reason}` if not.

## Examples

    iex> can_delete_user?(user_to_delete, current_user)
    :ok

    iex> can_delete_user?(current_user, current_user)
    {:error, :cannot_delete_self}

# `can_manage_user_credentials?`

True when `current_user` may manage `user`'s credentials — set a new password,
send a password-reset mail, or change the address those mails are delivered to.

Credential management is the one admin action that hands over an account, so it
is decided by ROLE and by RANK, not by the `users` permission that admits a
visitor to the user pages. Holding `users` answers "may this person administer
users at all"; it must never answer "may this person take over that particular
account". The rules mirror `can_delete_user?/2` (only an Owner acts on an
Admin) and the impersonation authority in `PhoenixKitWeb.Users.MultiSession`
(an Owner is never a target for anyone but themselves), so the three
account-takeover surfaces agree.

1. Your own account is always yours to manage.
2. The actor must hold Owner or Admin **by role**.
3. An Owner target may be managed only by an Owner.
4. An Admin target may be managed only by an Owner.

Everything else — an ordinary user changing their own password — belongs on
their own settings page, not here.

## Examples

    iex> can_manage_user_credentials?(some_user, admin)
    true

    iex> can_manage_user_credentials?(owner, admin)
    false

# `can_manage_user_status?`

True when `current_user` may activate or deactivate `user`.

Same authority as `can_manage_user_credentials?/2`, for the same reason:
deactivation is decided by rank, not by the `users` permission that admits a
visitor to the user pages. Without it a role holding only `users` can switch
off an Admin — or a non-last Owner — which is a denial of service against the
accounts that are meant to outrank it.

Unlike credential management, **your own status is not yours to change**. The
admin UI has always said so ("You cannot deactivate your own account for
security reasons") but two of the three pages enforced it only in the
template, and the third not at all — so the rule lived in markup a client
composes for itself. It lives here now.

The last-Owner protection in `Roles.can_deactivate_user?/1` is a separate,
target-only rule and still applies on top of this one.

# `change_account_type`

Changes a user's account type. Validates no members exist when switching org→person.

# `change_admin_note`

Returns a changeset for tracking admin note changes.

## Examples

    iex> change_admin_note(note)
    %Ecto.Changeset{}

# `change_user_email`

Returns an `%Ecto.Changeset{}` for changing the user email.

## Examples

    iex> change_user_email(user)
    %Ecto.Changeset{data: %User{}}

# `change_user_password`

Returns an `%Ecto.Changeset{}` for changing the user password.

## Examples

    iex> change_user_password(user)
    %Ecto.Changeset{data: %User{}}

# `change_user_profile`

Returns an `%Ecto.Changeset{}` for changing the user profile.

## Examples

    iex> change_user_profile(user)
    %Ecto.Changeset{data: %User{}}

# `change_user_registration`

Returns an `%Ecto.Changeset{}` for tracking user changes.

## Examples

    iex> change_user_registration(user)
    %Ecto.Changeset{data: %User{}}

# `confirm_user`

Confirms a user by the given token.

If the token matches, the user account is marked as confirmed
and the token is deleted.

# `confirm_user_from_external_proof`

```elixir
@spec confirm_user_from_external_proof(PhoenixKit.Users.Auth.User.t()) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, Ecto.Changeset.t()}
```

Confirms an account whose email ownership was just proven by an external
channel (verified OAuth email, magic link) rather than by the confirmation
email — and closes the account-pre-hijack window while doing so.

An unconfirmed row may have been *pre-registered by someone else* with a
password they know. Confirming it alone would hand that row to the rightful
address holder while leaving the pre-registrant's password and sessions
intact. So, atomically with the confirmation, every token (sessions, pending
resets, confirmation links) is deleted and the stored password hash is
rotated to an unguessable value. The rightful owner sets a password via
"forgot password" if they want one; they never knew the old one.

Already-confirmed users are returned unchanged.

# `create_admin_note`

Creates an admin note about a user.

## Parameters

- `user` - The user being noted about
- `author` - The admin creating the note
- `attrs` - Map containing `:content`

## Examples

    iex> create_admin_note(user, author, %{content: "Important note"})
    {:ok, %AdminNote{}}

    iex> create_admin_note(user, author, %{content: ""})
    {:error, %Ecto.Changeset{}}

# `create_guest_user`

Creates a guest user from checkout billing data.

This function is used during guest checkout to create a temporary user
account. The user will have `confirmed_at = nil` until they verify their
email address.

## Parameters

- `attrs` - Map with email (required), first_name, last_name

## Returns

- `{:ok, user}` - New user created successfully
- `{:error, :email_exists_confirmed}` - Email belongs to confirmed user (should login)
- `{:error, :email_exists_unconfirmed, existing_user}` - Reuse existing unconfirmed user
- `{:error, changeset}` - Validation errors

## Examples

    iex> create_guest_user(%{email: "guest@example.com", first_name: "John"})
    {:ok, %User{}}

    iex> create_guest_user(%{email: "existing@confirmed.com"})
    {:error, :email_exists_confirmed}

    iex> create_guest_user(%{email: "existing@unconfirmed.com"})
    {:error, :email_exists_unconfirmed, %User{}}

# `delete_admin_note`

Deletes an admin note.

## Examples

    iex> delete_admin_note(note)
    {:ok, %AdminNote{}}

# `delete_all_user_session_tokens`

Deletes all session tokens for the given user.

This function is useful when you need to force logout a user from all sessions,
for example when their roles change and they need fresh authentication.

# `delete_user`

Deletes a user account with proper cascade handling and data anonymization.

## Protection Rules

1. Cannot delete self - Prevents accidental self-deletion
2. Cannot delete last Owner - System must always have at least one Owner
3. Admin/Owner only - Only privileged users can delete accounts

## Data Handling Strategy

### Cascade Delete (automatic or manual)
- User tokens (ON DELETE CASCADE in DB)
- Role assignments (ON DELETE CASCADE in DB)
- OAuth providers
- Billing profiles
- Shop carts
- Admin notes

### Anonymize (preserve data, remove PII)
- Orders - SET NULL on user_uuid, preserve financial records
- Posts - Keep content, set user_uuid to NULL, mark as deleted author
- Comments - Keep content, set user_uuid to NULL, mark as deleted author
- Tickets - Preserve for support history, anonymize
- Email logs - Retain for compliance, anonymize
- Files - Anonymize ownership

## Parameters

- `user` - The user to delete
- `opts` - Options map containing:
  - `:current_user` - The user performing the deletion (required)
  - `:ip_address` - IP address for audit logging
  - `:user_agent` - User agent for audit logging

## Returns

- `{:ok, %{deleted_user_uuid: uuid, anonymized_records: count}}` - Success
- `{:error, :cannot_delete_self}` - Cannot delete your own account
- `{:error, :cannot_delete_last_owner}` - Cannot delete the last Owner
- `{:error, :insufficient_permissions}` - Current user lacks permission
- `{:error, reason}` - Other errors

## Examples

    iex> delete_user(user, %{current_user: admin_user})
    {:ok, %{deleted_user_uuid: "some-uuid", anonymized_records: 15}}

    iex> delete_user(user, %{current_user: user})
    {:error, :cannot_delete_self}

    iex> delete_user(last_owner, %{current_user: admin_user})
    {:error, :cannot_delete_last_owner}

    iex> delete_user(admin_user, %{current_user: non_owner_admin})
    {:error, :insufficient_permissions}

# `delete_user_custom_field`

```elixir
@spec delete_user_custom_field(PhoenixKit.Users.Auth.User.t(), String.t(), keyword()) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :not_found}
```

Deletes a specific custom field for a user.

Removes the key at the database level (`custom_fields - key`) — the
removal counterpart to `merge_user_custom_fields/3`, with the same
lost-update rationale: the historical Map.delete + whole-map replace
could silently drop a key a concurrent writer had just merged in.
Removing an absent key is a no-op that still returns `{:ok, user}`;
returns `{:error, :not_found}` if the user row was deleted
concurrently.

## Examples

    iex> delete_user_custom_field(user, "phone")
    {:ok, %User{}}

# `delete_user_session_token`

Deletes the signed token with the given context.

# `deliver_user_confirmation_instructions`

Delivers the confirmation email instructions to the given user.

## Examples

    iex> deliver_user_confirmation_instructions(user, &PhoenixKit.Utils.Routes.url("/users/confirm/#{&1}"))
    {:ok, %{to: ..., body: ...}}

    iex> deliver_user_confirmation_instructions(confirmed_user, &PhoenixKit.Utils.Routes.url("/users/confirm/#{&1}"))
    {:error, :already_confirmed}

# `deliver_user_reset_password_instructions`

Delivers the reset password email to the given user.

This function includes rate limiting protection to prevent mass password reset attacks.
After exceeding the rate limit (default: 3 requests per 5 minutes), subsequent
requests will be rejected with `{:error, :rate_limit_exceeded}`.

## Options

  * `:rate_limit` — set to `false` when the caller has ALREADY charged
    `RateLimiter.check_password_reset_rate_limit/1` for this request. The
    public forgot-password form has to throttle before the user lookup (a
    limiter that only runs for addresses which resolve to a user is an
    account-existence oracle), and both checks hit the same per-email bucket
    — so leaving this on there spends two of the three allowed hits per
    submission and silently drops every second reset email. Defaults to
    `true` for callers with no limiter of their own (e.g. the admin
    "send reset link" action).

## Examples

    iex> deliver_user_reset_password_instructions(user, &PhoenixKit.Utils.Routes.url("/users/reset-password/#{&1}"))
    {:ok, %{to: ..., body: ...}}

    iex> deliver_user_reset_password_instructions(user, &PhoenixKit.Utils.Routes.url("/users/reset-password/#{&1}"))
    {:error, :rate_limit_exceeded}

# `deliver_user_update_email_instructions`

Delivers the update email instructions to the given user.

## Examples

    iex> deliver_user_update_email_instructions(user, current_email, &PhoenixKit.Utils.Routes.url("/profile/settings/confirm-email/#{&1}"))
    {:ok, %{to: ..., body: ...}}

# `demote_to_user`

Demotes an admin user to regular user role.

## Examples

    iex> demote_to_user(user)
    {:ok, %RoleAssignment{}}

# `enforce_registration_account_type`

```elixir
@spec enforce_registration_account_type(map(), String.t()) :: map()
```

Stamps the account-type policy onto public-signup params, server-side.

⚠️ This — not the hidden `<select>` — is the control. `registration_changeset/3`
casts `account_type` and `organization_name` straight from the payload, and a
`phx-submit` payload is whatever the client sends: without this, a forged
`user[account_type]=organization` created an organization account on a site
where the picker was never rendered (including one with organization accounts
switched off entirely).

Mirrors `maybe_write_remember_me_cookie/3`: the policy is enforced where the
value is used, so no caller and no forged param can route around it.

In `"choice"` mode the visitor's pick is honoured but normalised to the two
known values — the registration changeset has no `validate_inclusion` of its
own, so an unknown string would otherwise reach the insert and be rejected by
a CHECK constraint as a 500 rather than a validation error.

# `ensure_active_user`

Ensures the user is active by checking the is_active field.

Returns nil for inactive users and logs a warning.
Returns the user for active users or nil input.

## Examples

    iex> ensure_active_user(%User{is_active: true})
    %User{is_active: true}

    iex> ensure_active_user(%User{is_active: false, uuid: "some-uuid"})
    nil

    iex> ensure_active_user(nil)
    nil

# `generate_user_session_token`

Generates a session token.

## Options

  * `:fingerprint` - Optional `%SessionFingerprint{}` struct with `:ip_address` and `:user_agent_hash`

## Examples

    # Without fingerprinting (backward compatible)
    token = generate_user_session_token(user)

    # With fingerprinting
    fingerprint = PhoenixKit.Utils.SessionFingerprint.create_fingerprint(conn)
    token = generate_user_session_token(user, fingerprint: fingerprint)

# `get_admin_note`

Gets a single admin note by UUID.

Preloads the author information.

## Examples

    iex> get_admin_note("01924...")
    %AdminNote{}

    iex> get_admin_note("nonexistent")
    nil

# `get_all_user_session_tokens`

Gets all active session tokens for the given user.

This is useful for finding all active sessions to broadcast logout messages.

# `get_first_admin`

Gets the first admin user (Owner or Admin role).

Useful for programmatic operations that require a user ID, such as
creating entities via scripts or seeds.

Returns the first Owner if one exists, otherwise the first Admin,
otherwise nil.

## Examples

    iex> get_first_admin()
    %User{id: 1, email: "admin@example.com"}

    iex> get_first_admin()
    nil  # No admin users exist

# `get_first_admin_uuid`

Gets the UUID of the first admin user.

Convenience function that returns just the user UUID, useful for
setting `created_by_uuid` fields programmatically.

## Examples

    iex> get_first_admin_uuid()
    "019b5704-3680-7b95-9d82-ef16127f1fd2"

    iex> get_first_admin_uuid()
    nil  # No admin users exist

# `get_first_user`

Gets the first user in the system (by insertion order).

Returns the earliest registered user (by UUID, which is time-ordered via UUIDv7).
Useful as a fallback when no specific admin is needed.

## Examples

    iex> get_first_user()
    %User{uuid: "some-uuid"}

# `get_first_user_uuid`

Gets the UUID of the first user in the system.

Convenience function for getting a user UUID for `created_by` fields.

Deprecated name kept for backwards compatibility - returns UUID now.
Prefer `get_first_user_uuid/0` for new code.

## Examples

    iex> get_first_user_uuid()
    "01924..."

# `get_role_stats`

Gets role statistics for dashboard display.

## Examples

    iex> get_role_stats()
    %{
      total_users: 10,
      owner_count: 1,
      admin_count: 2,
      user_count: 7
    }

# `get_session_token_record`

Gets the user token record for the given session token.

This is useful for accessing fingerprint data stored with the token.

## Examples

    iex> get_session_token_record("valid_token")
    %UserToken{ip_address: "192.168.1.1", user_agent_hash: "abc123"}

    iex> get_session_token_record("invalid_token")
    nil

# `get_user`

Gets a single user.

Returns `nil` if the user does not exist.

## Examples

    iex> get_user(123)
    %User{}

    iex> get_user(456)
    nil

# `get_user!`

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_email`

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`

Gets a user by email and password.

This function includes rate limiting protection to prevent brute-force attacks.
After exceeding the rate limit (default: 5 attempts per minute), subsequent
attempts will be rejected with `{:error, :rate_limit_exceeded}`.

## Examples

    iex> get_user_by_email_and_password("foo@example.com", "correct_password")
    {:ok, %User{}}

    iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
    {:error, :invalid_credentials}

    iex> get_user_by_email_and_password("foo@example.com", "password", "192.168.1.1")
    {:ok, %User{}}

# `get_user_by_email_or_username`

Gets a user by email or username.

Checks if the input contains "@" to determine whether to search
by email or username.

## Examples

    iex> get_user_by_email_or_username("user@example.com")
    %User{}

    iex> get_user_by_email_or_username("johndoe")
    %User{}

    iex> get_user_by_email_or_username("unknown")
    nil

# `get_user_by_email_or_username_and_password`

Gets a user by email or username and password.

Allows users to log in using either their email address or username.
If the input contains "@", it's treated as an email; otherwise, as a username.
Username lookup is case-insensitive for better UX.

This function includes rate limiting protection to prevent brute-force attacks.

## Examples

    iex> get_user_by_email_or_username_and_password("foo@example.com", "correct_password")
    {:ok, %User{}}

    iex> get_user_by_email_or_username_and_password("johndoe", "correct_password")
    {:ok, %User{}}

    iex> get_user_by_email_or_username_and_password("JohnDoe", "correct_password")
    {:ok, %User{}}  # Case-insensitive username lookup

    iex> get_user_by_email_or_username_and_password("unknown", "password")
    {:error, :invalid_credentials}

# `get_user_by_reset_password_token`

Gets the user by reset password token.

## Examples

    iex> get_user_by_reset_password_token("validtoken")
    %User{}

    iex> get_user_by_reset_password_token("invalidtoken")
    nil

# `get_user_by_session_token`

Gets the user with the given signed token.

# `get_user_by_username`

Gets a user by username.

## Examples

    iex> get_user_by_username("johndoe")
    %User{}

    iex> get_user_by_username("unknown")
    nil

# `get_user_custom_field`

Gets a specific custom field value for a user.

Returns the value if the key exists, or nil otherwise.

## Examples

    iex> get_user_custom_field(user, "phone")
    "555-1234"

    iex> get_user_custom_field(user, "nonexistent")
    nil

# `get_user_custom_field_display`

Gets the display value for a custom field, resolving select field indexes to text.

For select/radio/checkbox fields that store index values (0, 1, 2...),
this function returns the actual option text. For other fields, returns
the raw value.

## Examples

    iex> get_user_custom_field_display(user, "favorite_color")
    "Blue"  # even though stored value is "1"

    iex> get_user_custom_field_display(user, "phone")
    "555-1234"  # non-select field returns raw value

# `get_user_field`

Gets a user field value from either schema fields or custom fields.

This unified accessor provides O(1) performance by checking struct fields
first using Map.has_key?/2, then falling back to custom_fields JSONB.

Certain sensitive fields are excluded for security:
- password, current_password (virtual fields)
- hashed_password (use authentication functions instead)

## Examples

    # Standard schema fields (O(1) struct access)
    iex> get_user_field(user, "email")
    "user@example.com"

    iex> get_user_field(user, :first_name)
    "John"

    # Custom fields (O(1) JSONB lookup)
    iex> get_user_field(user, "phone")
    "555-1234"

    # Nonexistent returns nil
    iex> get_user_field(user, "nonexistent")
    nil

    # Excluded sensitive fields return nil
    iex> get_user_field(user, "hashed_password")
    nil

## Performance

- Standard fields: ~0.5μs (direct struct access)
- Custom fields: ~1-2μs (JSONB lookup)
- No performance penalty from checking both locations

# `get_user_for_selection`

Gets a user by UUID with minimal fields for selection interfaces.

Returns a user map with uuid, email, first_name, and last_name fields.
Returns nil if user is not found.

## Examples

    iex> PhoenixKit.Users.Auth.get_user_for_selection("01924...")
    %{uuid: "01924...", email: "user@example.com", first_name: "John", last_name: "Doe"}

    iex> PhoenixKit.Users.Auth.get_user_for_selection("nonexistent")
    nil

# `get_user_roles`

Gets all active roles for a user.

## Examples

    iex> get_user_roles(user)
    ["Admin", "User"]

    iex> get_user_roles(user_with_no_roles)
    []

# `get_user_with_roles`

Gets a user by UUID with preloaded roles.

## Examples

    iex> get_user_with_roles("01924...")
    %User{roles: [%Role{}, %Role{}]}

    iex> get_user_with_roles("nonexistent")
    nil

# `get_users_by_ids`

Gets users by list of UUIDs.

Returns list of users with all fields including custom_fields.
Useful for batch loading users when you have a list of UUIDs.

## Examples

    iex> get_users_by_ids(["01924...", "01925..."])
    [%User{uuid: "01924...", ...}, %User{uuid: "01925...", ...}]

    iex> get_users_by_ids([])
    []

# `get_users_by_uuids`

Gets multiple users by their UUIDs.

# `list_admin_notes`

Lists all admin notes for a user, ordered by most recent first.

Preloads the author information for display.

## Examples

    iex> list_admin_notes(user)
    [%AdminNote{}, ...]

# `list_available_members_for_organization`

Lists person users available to join an organization (not already in one).
Excludes the organization itself and users already belonging to any organization.

# `list_organization_members`

Lists all person users belonging to an organization.

# `list_organizations`

Lists all organization-type users.

# `list_roles`

Lists all roles.

## Examples

    iex> list_roles()
    [%Role{}, %Role{}, %Role{}]

# `list_users_paginated`

Lists users with pagination and optional role filtering.

## Examples

    iex> list_users_paginated(page: 1, page_size: 10)
    %{users: [%User{}], total_count: 50, total_pages: 5}

    iex> list_users_paginated(page: 1, page_size: 10, role: "Admin")
    %{users: [%User{}], total_count: 3, total_pages: 1}

# `merge_user_custom_fields`

```elixir
@spec merge_user_custom_fields(PhoenixKit.Users.Auth.User.t(), map(), keyword()) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :not_found}
```

Atomically merges `additions` into a user's custom_fields JSONB column
at the database level (`custom_fields || additions`), instead of
`update_user_custom_fields/3`'s read-modify-write contract.

Every existing caller that wants to add/update a couple of keys while
preserving the rest follows the same pattern: fetch the user, compute
`Map.merge(user.custom_fields, %{"key" => value})` in Elixir, then call
`update_user_custom_fields/3` with the merged result. That has a real
lost-update race: two callers merging DIFFERENT keys into the same
user's custom_fields concurrently (say, a locale preference switch and
a newsletters opt-out) can each read the same pre-update snapshot, and
whichever write commits second silently overwrites the whole column
with a map that never saw the other's key — no error, no conflict
raised, just quietly missing data. Doing the merge inside the UPDATE
statement itself closes that window: Postgres serializes concurrent
writers on the same row, so the second UPDATE's `||` reads the FIRST
writer's already-committed value, not a stale snapshot.

Only ever ADDS or OVERWRITES the given keys — this can't clear/remove
one (unlike `update_user_custom_fields/3`, which replaces the whole
map and so CAN clear fields by omitting them from the replacement —
see that function's own tests); to remove a single key atomically use
`delete_user_custom_field/3`. Reach for this whenever the intent is
"add/update these specific keys, leave everything else exactly as any
concurrent writer left it"; reach for `update_user_custom_fields/3`
when the caller genuinely needs to replace the whole map.

Returns `{:error, :not_found}` rather than raising if the user row was
deleted concurrently between the caller's read and this call.

## Examples

    iex> merge_user_custom_fields(user, %{"newsletters_opted_out_at" => "2026-01-01T00:00:00Z"})
    {:ok, %User{}}

# `organization_accounts_enabled?`

```elixir
@spec organization_accounts_enabled?() :: boolean()
```

Whether organization accounts are enabled site-wide
(`"enable_organization_accounts"`, default `false`).

The master switch: off, this install has person accounts only and the
organization columns, tabs and pickers are hidden throughout the admin area.

# `promote_to_admin`

Promotes a user to admin role.

## Examples

    iex> promote_to_admin(user)
    {:ok, %RoleAssignment{}}

    iex> promote_to_admin(user, assigned_by_user)
    {:ok, %RoleAssignment{}}

# `register_user`

Registers a user with automatic role assignment.

Role assignment is handled by Elixir application logic:
- First user receives Owner role
- Subsequent users receive User role
- Uses database transactions to prevent race conditions

This function includes rate limiting protection to prevent spam account creation.
Rate limits apply per email address and optionally per IP address.

## Examples

    iex> register_user(%{field: value})
    {:ok, %User{}}

    iex> register_user(%{field: bad_value})
    {:error, %Ecto.Changeset{}}

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

    iex> register_user(%{email: "user@example.com", password: "pass", custom_fields: %{"source" => "landing_page"}})
    {:ok, %User{}}

# `register_user_with_geolocation`

Registers a user with IP geolocation data.

This function attempts to look up geographical location information
based on the provided IP address and includes it in the user registration.
If geolocation lookup fails, the user is still registered with just the IP address.

This function automatically applies rate limiting based on the IP address.

## Examples

    iex> register_user_with_geolocation(%{email: "user@example.com", password: "password"}, "192.168.1.1")
    {:ok, %User{registration_ip: "192.168.1.1", registration_country: "United States"}}

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

# `registration_account_type`

```elixir
@spec registration_account_type() :: String.t()
```

The account-type policy for the PUBLIC signup forms.

Returns one of:

  * `"choice"` — the visitor picks Personal or Organization (the default, and
    what every install that has never touched the setting reads).
  * `"person"` — every self-serve signup is a person. Organizations still
    exist; only an admin creates them.
  * `"organization"` — every self-serve signup IS an organization, and the
    Organization Name field is required. The B2B shape: there are no personal
    accounts to open, and staff arrive through an organization invitation.

Collapses to `"person"` whenever organization accounts are off, so a single
call answers "what may this form create?" without every caller also having to
ask `organization_accounts_enabled?/0`.

An unrecognised stored value falls back to the default rather than raising —
a bad row must not take the signup page down.

# `remove_from_organization`

Removes a user from their organization.

# `remove_role`

Removes a role from a user.

## Examples

    iex> remove_role(user, "Admin")
    {:ok, %RoleAssignment{}}

    iex> remove_role(user, "NonexistentRole")
    {:error, :assignment_not_found}

# `reset_user_password`

Resets the user password.

## Examples

    iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"})
    {:ok, %User{}}

    iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
    {:error, %Ecto.Changeset{}}

# `run_before_user_delete_hooks`

Runs every discovered module's optional `before_user_delete/1` hook.
Public with an injectable module list so the dispatch is testable;
production callers use the discovered default.

# `search_users`

Searches users by email or name for selection interfaces.

Returns a list of users matching the search term, limited to 10 results
for performance. Useful for autocomplete/typeahead interfaces.

## Examples

    iex> PhoenixKit.Users.Auth.search_users("john")
    [%User{email: "john@example.com", first_name: "John"}, ...]

    iex> PhoenixKit.Users.Auth.search_users("")
    []

# `set_organization`

Sets a person user's organization. Validates target is an organization-type user.

# `set_user_custom_field`

Sets a specific custom field value for a user.

Updates a single key in the custom_fields map while preserving other fields.

## Examples

    iex> set_user_custom_field(user, "phone", "555-1234")
    {:ok, %User{}}

    iex> set_user_custom_field(user, "department", "Product")
    {:ok, %User{}}

Returns `{:error, :not_found}` (not a changeset error) if the user
row was deleted concurrently — same contract as
`merge_user_custom_fields/3`, which this delegates to.

# `toggle_user_confirmation`

Toggles user confirmation status (admin function).

## Authorization

Pass `actor: %User{}` and the RANK rule is enforced here, in the context —
the actor must outrank the target (`can_manage_user_status?/2`). Do that from
every request-driven path.

Omitting `:actor` is the system path and performs NO authorization, matching
`update_user_status/3`. An `:actor` that is PRESENT but is not a `%User{}` is
refused rather than read as absent.

## Examples

    iex> toggle_user_confirmation(confirmed_user)
    {:ok, %User{confirmed_at: nil}}

    iex> toggle_user_confirmation(unconfirmed_user)
    {:ok, %User{confirmed_at: ~N[2023-01-01 12:00:00]}}

    iex> toggle_user_confirmation(owner, actor: admin)
    {:error, :insufficient_permissions}

# `updatable_profile_fields`

The schema fields `update_user_fields/2` routes OUT of `custom_fields` and
writes through `profile_changeset`.

Public so that callers filtering untrusted params against this rule read the
list rather than restating it.

# `update_admin_note`

Updates an admin note.

Only the content can be updated.

## Examples

    iex> update_admin_note(note, %{content: "Updated note"})
    {:ok, %AdminNote{}}

    iex> update_admin_note(note, %{content: ""})
    {:error, %Ecto.Changeset{}}

# `update_user_avatar`

Update a user's avatar by storing the file and saving the file ID.

This function handles the complete avatar upload workflow:
1. Stores the file in configured storage buckets
2. Automatically queues background job for variant generation
3. Saves the file ID to the user's custom_fields

This is a convenience function that combines file storage with user update.
Can be called from any context (LiveView, controllers, scripts, etc.) outside
of the PhoenixKit project.

## Parameters
- `user` - The User struct to update
- `file_path` - Path to the uploaded file (temporary location)
- `filename` - Original filename for the upload
- `user_uuid` - The user UUID owning this file (defaults to user.uuid)

## Returns
- `{:ok, user}` - Avatar saved successfully
- `{:error, reason}` - File storage or update failed

## Examples

    # Store avatar in default location with automatic variant generation
    {:ok, updated_user} = Auth.update_user_avatar(user, "/tmp/upload_xyz", "avatar.jpg")

    # Store with explicit user_uuid (for custom workflows)
    {:ok, updated_user} = Auth.update_user_avatar(user, "/tmp/upload_xyz", "avatar.jpg", custom_user_uuid)

## Automatically Generated Variants
The storage layer automatically generates these image variants:
- original - Full-size image
- large - 800x800px
- medium - 400x400px
- small - 200x200px
- thumbnail - 100x100px

# `update_user_custom_fields`

Updates user custom fields.

Custom fields are stored as JSONB and can contain arbitrary key-value pairs
for extending user data without schema changes.

## Examples

    iex> update_user_custom_fields(user, %{"phone" => "555-1234", "department" => "Engineering"})
    {:ok, %User{}}

    iex> update_user_custom_fields(user, "invalid")
    {:error, %Ecto.Changeset{}}

# `update_user_email`

Updates the user email using the given token.

If the token matches, the user email is updated and the token is deleted.
The confirmed_at date is also updated to the current time.

# `update_user_fields`

Updates both schema and custom fields in a single call.

This is a unified update function that automatically splits the provided
attributes into schema fields and custom fields, updating both appropriately.

## Schema Fields
- first_name, last_name, email, username, user_timezone

## Custom Fields
- Any other keys are treated as custom fields

## Examples

    iex> update_user_fields(user, %{
    ...>   "first_name" => "John",
    ...>   "email" => "john@example.com",
    ...>   "phone" => "555-1234",
    ...>   "department" => "Engineering"
    ...> })
    {:ok, %User{}}

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

## ⚠️ This function does not authorize, and `email` is a credential

A key named in `updatable_profile_fields/0` leaves `custom_fields` and is
written to the schema — including `:email`, with **no** confirmation-token
flow, and `:username`, which is the second credential
`get_user_by_email_or_username_and_password/3` accepts. Whoever may call this
may re-point the address a password-reset link is delivered to. Callers that
build `attrs` from request params must filter those names first (see
`PhoenixKitWeb.Users.UserForm`) or gate the call on
`can_manage_user_credentials?/2`.

# `update_user_locale_preference`

```elixir
@spec update_user_locale_preference(PhoenixKit.Users.Auth.User.t(), String.t() | nil) ::
  {:ok, PhoenixKit.Users.Auth.User.t()}
  | {:error, String.t()}
  | {:error, :not_found}
```

Updates user's preferred locale (dialect preference).

This allows users to select specific language dialects (e.g., en-GB, en-US)
while URLs continue to use base codes (e.g., /en/).
The locale is stored in the `custom_fields` JSONB column.

Writes through the atomic single-key primitives —
`merge_user_custom_fields/3` to set, `delete_user_custom_field/3` to
clear — so a concurrent writer of a different custom_fields key is
never lost. Passes `ensure_definitions: false` deliberately: the
locale is an internal preference, not an admin-managed custom field,
so the first write no longer auto-registers a field definition
(the old whole-map path did, as a side effect).

## Examples

    iex> update_user_locale_preference(user, "en-GB")
    {:ok, %User{custom_fields: %{"preferred_locale" => "en-GB", ...}}}

    iex> update_user_locale_preference(user, "invalid")
    {:error, "must be a valid locale format (e.g., en-US, es-MX)"}

    iex> update_user_locale_preference(user, nil)
    {:ok, %User{...}}  # Clears the preference

Two distinct error shapes: a validation failure returns `{:error, message}`
with a human-readable string, while a row deleted concurrently surfaces the
primitives' `{:error, :not_found}`.

# `update_user_password`

Updates the user password.

## Examples

    iex> update_user_password(user, "valid password", %{password: ...})
    {:ok, %User{}}

    iex> update_user_password(user, "invalid password", %{password: ...})
    {:error, %Ecto.Changeset{}}

# `update_user_profile`

Updates a user's profile information.

## Examples

    iex> update_user_profile(user, %{first_name: "John", last_name: "Doe"})
    {:ok, %User{}}

    iex> update_user_profile(user, %{first_name: ""})
    {:error, %Ecto.Changeset{}}

# `update_user_status`

Updates user status with Owner protection.

Prevents deactivation of the last Owner to maintain system security.

## Authorization

Pass `actor: %User{}` and the RANK rule is enforced here, in the context —
the actor must outrank the target (`can_manage_user_status?/2`). Do that from
every request-driven path. The rank check lived only in the admin edit form
once, and the user-list and user-detail pages reached this function without
it, so a role holding merely the `users` permission could deactivate an Admin
or a non-last Owner from two of the three pages. Enforcing it here is what
makes a fourth caller safe by construction.

Omitting `:actor` means "system-initiated" and performs NO authorization —
correct for `PhoenixKit.Users.Referrals` expiring an account, wrong for
anything a user asked for. An `:actor` that is PRESENT but is not a `%User{}`
is refused rather than read as absent: it is a caller that meant to supply an
actor and got the shape wrong.

## Parameters

- `user`: User to update
- `attrs`: Status attributes (typically %{"is_active" => true/false})
- `opts`: `:actor` — the `%User{}` performing the change, when there is one

## Examples

    iex> update_user_status(user, %{"is_active" => false}, actor: admin)
    {:ok, %User{}}

    iex> update_user_status(owner, %{"is_active" => false}, actor: admin)
    {:error, :insufficient_permissions}

    iex> update_user_status(last_owner, %{"is_active" => false})
    {:error, :cannot_deactivate_last_owner}

# `user_has_role?`

Checks if a user has a specific role.

## Examples

    iex> user_has_role?(user, "Admin")
    true

    iex> user_has_role?(user, "Owner")
    false

# `users_with_role`

Gets all users who have a specific role.

## Examples

    iex> users_with_role("Admin")
    [%User{}, %User{}]

    iex> users_with_role("NonexistentRole")
    []

# `verify_session_fingerprint`

Verifies a session fingerprint against the stored token data.

Returns:
- `:ok` if fingerprint matches or fingerprinting is disabled
- `{:warning, reason}` if there's a partial mismatch (IP or UA changed)
- `{:error, :fingerprint_mismatch}` if both IP and UA changed

## Examples

    iex> verify_session_fingerprint(conn, token)
    :ok

    iex> verify_session_fingerprint(conn, token)
    {:warning, :ip_mismatch}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
