---
name: phoenix-auth-customization
description: Use when extending phx.gen.auth — adding registration fields, custom user attributes, extra migrations alongside generated auth, fixture updates.
file_patterns:
  - "**/accounts.ex"
  - "**/accounts/*.ex"
  - "**/user.ex"
  - "**/user_registration_live.ex"
  - "**/user_live/registration.ex"
auto_suggest: true
---

# Phoenix Auth Customization

## RULES — Follow these with no exceptions

1. **Never modify generated auth migrations** — create separate migrations for custom fields; generated migrations are tested and correct
2. **Update `registration_changeset` to cast and validate new fields** — don't create a separate changeset for initial registration
3. **Update test fixtures when adding required fields** — missing fixture fields cause cryptic test failures across the entire test suite
4. **Confirm users in test fixtures for password-based auth** — set `confirmed_at: DateTime.utc_now(:second)` or tests requiring authenticated users will fail
5. **Update both the registration form AND the `save/2` handler** — the form must send the field, and the handler must pass it to the context
6. **Use `unique_constraint` + database unique index for uniqueness** — never validate uniqueness in application code alone

---

## Running phx.gen.auth

Start with the generator, then extend. Never hand-roll auth.

```bash
# Generate auth with LiveView (recommended)
mix phx.gen.auth Accounts User users

# This creates:
# - Migration: priv/repo/migrations/*_create_users_auth_tables.exs
# - Schema: lib/my_app/accounts/user.ex
# - Scope: lib/my_app/accounts/scope.ex
# - Context: lib/my_app/accounts.ex
# - LiveViews: lib/my_app_web/live/user_live/registration.ex, login.ex,
#              confirmation.ex, settings.ex
# - Controller: lib/my_app_web/controllers/user_session_controller.ex
# - Plugs: lib/my_app_web/user_auth.ex
# - Tests: test/my_app/accounts_test.exs, test/my_app_web/live/user_live/*_test.exs
```

Phoenix 1.8's `phx.gen.auth` defaults to **magic-link (passwordless) auth** — registration has no password field unless you pass options — and generates a `MyApp.Accounts.Scope` module consumed as `@current_scope`.

---

## Adding Custom Fields

### Step 1: Create a Separate Migration

```bash
mix ecto.gen.migration add_username_to_users
```

```elixir
defmodule MyApp.Repo.Migrations.AddUsernameToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :username, :string, null: false
    end

    create unique_index(:users, [:username])
  end
end
```

### Step 2: Update the Schema

```elixir
defmodule MyApp.Accounts.User do
  schema "users" do
    field :email, :string
    field :username, :string  # Add new field
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :utc_datetime

    timestamps()
  end

  # Update registration_changeset to include username
  def registration_changeset(user, attrs, opts \\ []) do
    user
    |> cast(attrs, [:email, :username, :password])
    |> validate_required([:username])
    |> validate_username()
    |> validate_email(opts)
    |> validate_password(opts)
  end

  defp validate_username(changeset) do
    changeset
    |> validate_required([:username])
    |> validate_format(:username, ~r/^[a-zA-Z0-9_]+$/,
      message: "only letters, numbers, and underscores"
    )
    |> validate_length(:username, min: 3, max: 30)
    |> unsafe_validate_unique(:username, MyApp.Repo)
    |> unique_constraint(:username)
  end
end
```

### Step 3: Update the Registration LiveView

```elixir
# In lib/my_app_web/live/user_live/registration.ex — update the form
def render(assigns) do
  ~H"""
  <%!-- Phoenix 1.8 removed the old simple-form component; use <.form> --%>
  <.form for={@form} id="registration_form" phx-submit="save" phx-change="validate">
    <.input field={@form[:email]} type="email" label="Email" required />
    <.input field={@form[:username]} type="text" label="Username" required />
    <.input field={@form[:password]} type="password" label="Password" required />
    <:actions>
      <.button phx-disable-with="Creating account..." class="w-full">
        Create an account
      </.button>
    </:actions>
  </.form>
  """
end

# Update the save handler to pass username
def handle_event("save", %{"user" => user_params}, socket) do
  case Accounts.register_user(user_params) do
    {:ok, user} ->
      # ... existing logic
    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
  end
end
```

---

## Updating Test Fixtures

This is the most commonly missed step. Every test that creates a user will break if fixtures don't include new required fields.

```elixir
defmodule MyApp.AccountsFixtures do
  def unique_user_email, do: "user#{System.unique_integer()}@example.com"
  def unique_user_username, do: "user#{System.unique_integer([:positive])}"

  def valid_user_attributes(attrs \\ %{}) do
    Enum.into(attrs, %{
      email: unique_user_email(),
      username: unique_user_username(),  # Add new required field
      password: "hello world!"
    })
  end

  def user_fixture(attrs \\ %{}) do
    {:ok, user} =
      attrs
      |> valid_user_attributes()
      |> MyApp.Accounts.register_user()

    # Confirm user for password-based auth
    {:ok, user} =
      user
      |> Ecto.Changeset.change(%{confirmed_at: DateTime.utc_now(:second)})
      |> MyApp.Repo.update()

    user
  end
end
```

### Why Confirmation Matters

Without `confirmed_at`, the generated auth code treats the user as unconfirmed. Tests that log in users will silently fail or return unexpected redirects.

```elixir
# Bad — user is unconfirmed, login tests may fail
def user_fixture(attrs \\ %{}) do
  {:ok, user} =
    attrs
    |> valid_user_attributes()
    |> MyApp.Accounts.register_user()

  user  # Missing confirmation!
end

# Good — user is confirmed and ready for auth tests
def user_fixture(attrs \\ %{}) do
  {:ok, user} =
    attrs
    |> valid_user_attributes()
    |> MyApp.Accounts.register_user()

  {:ok, user} =
    user
    |> Ecto.Changeset.change(%{confirmed_at: DateTime.utc_now(:second)})
    |> MyApp.Repo.update()

  user
end
```

---

## Adding Profile Fields Later

For non-auth fields (bio, avatar, display name), create a separate `profile_changeset`:

```elixir
# In user.ex
def profile_changeset(user, attrs) do
  user
  |> cast(attrs, [:bio, :display_name, :avatar_url])
  |> validate_length(:bio, max: 500)
  |> validate_length(:display_name, max: 50)
end

# In accounts.ex
def update_user_profile(user, attrs) do
  user
  |> User.profile_changeset(attrs)
  |> Repo.update()
end
```

---

## Testing Auth Customization

```elixir
describe "register_user/1" do
  test "requires username" do
    {:error, changeset} = Accounts.register_user(%{
      email: "test@example.com",
      password: "validpassword123"
    })

    assert "can't be blank" in errors_on(changeset).username
  end

  test "validates username format" do
    {:error, changeset} = Accounts.register_user(%{
      email: "test@example.com",
      username: "has spaces",
      password: "validpassword123"
    })

    assert "only letters, numbers, and underscores" in errors_on(changeset).username
  end

  test "enforces unique username" do
    %{username: username} = user_fixture()

    {:error, changeset} = Accounts.register_user(%{
      email: "other@example.com",
      username: username,
      password: "validpassword123"
    })

    assert "has already been taken" in errors_on(changeset).username
  end
end
```

---

See `ecto-changeset-patterns` skill for advanced changeset composition.
See `phoenix-liveview-auth` skill for on_mount and auth redirect patterns.
See `testing-essentials` skill for comprehensive testing patterns.
