# Sudo Mode MFA-Only Verification Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace full login requirement for sudo mode with TOTP-only verification at dedicated `/users/sudo-verify` endpoint.

**Architecture:** Add new controller for sudo verification, modify UserAuth plugs to redirect to TOTP-only page instead of full login, add Accounts functions for granting sudo mode and verifying TOTP without recovery codes.

**Tech Stack:** Phoenix 1.8, Ecto, Phoenix.Controller, Plug, ExUnit

---

## Task 1: Add Accounts.grant_sudo_mode/1 function

**Files:**
- Modify: `lib/towerops/accounts/user.ex`
- Modify: `lib/towerops/accounts.ex`
- Create: `test/towerops/accounts/grant_sudo_mode_test.exs`

**Step 1: Write the failing test**

Create `test/towerops/accounts/grant_sudo_mode_test.exs`:

```elixir
defmodule Towerops.Accounts.GrantSudoModeTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures

  alias Towerops.Accounts

  describe "grant_sudo_mode/1" do
    test "updates user's last_sudo_at timestamp" do
      user = user_fixture()
      assert is_nil(user.last_sudo_at)

      {:ok, updated_user} = Accounts.grant_sudo_mode(user)

      assert %DateTime{} = updated_user.last_sudo_at
      assert DateTime.diff(DateTime.utc_now(), updated_user.last_sudo_at, :second) < 2
    end

    test "returns error if user is invalid" do
      user = user_fixture()
      # Pass invalid user struct
      assert {:error, _changeset} = Accounts.grant_sudo_mode(%{user | id: nil})
    end
  end
end
```

**Step 2: Run test to verify it fails**

Run: `mix test test/towerops/accounts/grant_sudo_mode_test.exs`

Expected: FAIL with "function Accounts.grant_sudo_mode/1 is undefined"

**Step 3: Add sudo_changeset to User schema**

In `lib/towerops/accounts/user.ex`, add after the existing changesets:

```elixir
@doc """
Changeset for updating sudo mode timestamp.
"""
def sudo_changeset(user, attrs) do
  user
  |> cast(attrs, [:last_sudo_at])
  |> validate_required([:last_sudo_at])
end
```

**Step 4: Add grant_sudo_mode/1 to Accounts context**

In `lib/towerops/accounts.ex`, add after the `sudo_mode?/2` function:

```elixir
@doc """
Grants sudo mode to a user by updating their last_sudo_at timestamp.

Returns `{:ok, user}` on success or `{:error, changeset}` on failure.

## Examples

    iex> grant_sudo_mode(user)
    {:ok, %User{last_sudo_at: ~U[2026-02-01 12:00:00Z]}}
"""
def grant_sudo_mode(%User{} = user) do
  now = DateTime.utc_now()

  user
  |> User.sudo_changeset(%{last_sudo_at: now})
  |> Repo.update()
end
```

**Step 5: Run test to verify it passes**

Run: `mix test test/towerops/accounts/grant_sudo_mode_test.exs`

Expected: PASS (2 tests)

**Step 6: Commit**

```bash
git add lib/towerops/accounts/user.ex lib/towerops/accounts.ex test/towerops/accounts/grant_sudo_mode_test.exs
git commit -m "feat: add Accounts.grant_sudo_mode/1 function"
```

---

## Task 2: Add Accounts.verify_totp_only/2 function

**Files:**
- Modify: `lib/towerops/accounts.ex`
- Create: `test/towerops/accounts/verify_totp_only_test.exs`

**Step 1: Write the failing test**

Create `test/towerops/accounts/verify_totp_only_test.exs`:

```elixir
defmodule Towerops.Accounts.VerifyTotpOnlyTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures

  alias Towerops.Accounts

  describe "verify_totp_only/2" do
    setup do
      user = user_fixture()

      # Create TOTP device
      {:ok, device} =
        Accounts.create_user_totp_device(user, %{
          name: "Test Device",
          secret: NimbleTOTP.secret()
        })

      %{user: user, device: device}
    end

    test "accepts valid TOTP codes", %{user: user, device: device} do
      code = NimbleTOTP.verification_code(device.secret)

      assert {:ok, returned_user} = Accounts.verify_totp_only(user, code)
      assert returned_user.id == user.id
    end

    test "rejects invalid TOTP codes", %{user: user} do
      assert {:error, :invalid_code} = Accounts.verify_totp_only(user, "000000")
    end

    test "rejects recovery codes", %{user: user} do
      # Recovery codes are longer than 6 digits
      recovery_code = "ABCD-EFGH-IJKL"

      assert {:error, :recovery_code_not_allowed} =
        Accounts.verify_totp_only(user, recovery_code)
    end

    test "rejects codes longer than 6 digits", %{user: user} do
      assert {:error, :recovery_code_not_allowed} =
        Accounts.verify_totp_only(user, "1234567890")
    end
  end
end
```

**Step 2: Run test to verify it fails**

Run: `mix test test/towerops/accounts/verify_totp_only_test.exs`

Expected: FAIL with "function Accounts.verify_totp_only/2 is undefined"

**Step 3: Add verify_totp_only/2 to Accounts context**

In `lib/towerops/accounts.ex`, add after the `verify_user_mfa/2` function:

```elixir
@doc """
Verifies a TOTP code for a user, rejecting recovery codes.

This is used for sudo mode verification where we want to ensure
the user has access to their authenticator device, not just a recovery code.

Returns `{:ok, user}` on success with valid TOTP code.
Returns `{:error, :recovery_code_not_allowed}` if a recovery code is provided.
Returns `{:error, reason}` for invalid TOTP codes.

## Examples

    iex> verify_totp_only(user, "123456")
    {:ok, %User{}}

    iex> verify_totp_only(user, "ABCD-EFGH-IJKL")
    {:error, :recovery_code_not_allowed}

    iex> verify_totp_only(user, "000000")
    {:error, :invalid_code}
"""
def verify_totp_only(%User{} = user, code) when is_binary(code) do
  # Reject codes longer than 6 digits (likely recovery codes)
  if String.length(String.replace(code, ~r/[^0-9]/, "")) > 6 do
    {:error, :recovery_code_not_allowed}
  else
    case verify_user_mfa(user, code) do
      {:ok, user, :totp} -> {:ok, user}
      {:ok, _user, :recovery_code} -> {:error, :recovery_code_not_allowed}
      {:error, reason} -> {:error, reason}
    end
  end
end
```

**Step 4: Run test to verify it passes**

Run: `mix test test/towerops/accounts/verify_totp_only_test.exs`

Expected: PASS (4 tests)

**Step 5: Commit**

```bash
git add lib/towerops/accounts.ex test/towerops/accounts/verify_totp_only_test.exs
git commit -m "feat: add Accounts.verify_totp_only/2 function"
```

---

## Task 3: Create UserSudoController and HTML module

**Files:**
- Create: `lib/towerops_web/controllers/user_sudo_controller.ex`
- Create: `lib/towerops_web/controllers/user_sudo_html.ex`
- Create: `lib/towerops_web/controllers/user_sudo_html/verify.html.heex`
- Create: `test/towerops_web/controllers/user_sudo_controller_test.exs`

**Step 1: Write the failing test**

Create `test/towerops_web/controllers/user_sudo_controller_test.exs`:

```elixir
defmodule ToweropsWeb.UserSudoControllerTest do
  use ToweropsWeb.ConnCase, async: true

  import Towerops.AccountsFixtures

  alias Towerops.Accounts

  setup do
    user = user_fixture()

    # Create TOTP device
    {:ok, device} =
      Accounts.create_user_totp_device(user, %{
        name: "Test Device",
        secret: NimbleTOTP.secret()
      })

    %{user: user, device: device}
  end

  describe "GET /users/sudo-verify" do
    test "redirects to login when not authenticated", %{conn: conn} do
      conn = get(conn, ~p"/users/sudo-verify")
      assert redirected_to(conn) == ~p"/users/log-in"
    end

    test "shows verification form for authenticated users", %{conn: conn, user: user} do
      conn = conn |> log_in_user(user) |> get(~p"/users/sudo-verify")

      response = html_response(conn, 200)
      assert response =~ "Verify Your Identity"
      assert response =~ "Enter the code from your authenticator app"
    end

    test "redirects to settings if already in sudo mode", %{conn: conn, user: user} do
      {:ok, user} = Accounts.grant_sudo_mode(user)

      conn = conn |> log_in_user(user) |> get(~p"/users/sudo-verify")

      assert redirected_to(conn) == ~p"/users/settings"
    end

    test "redirects to return path if already in sudo mode", %{conn: conn, user: user} do
      {:ok, user} = Accounts.grant_sudo_mode(user)

      conn =
        conn
        |> log_in_user(user)
        |> put_session(:user_return_to, "/some/path")
        |> get(~p"/users/sudo-verify")

      assert redirected_to(conn) == "/some/path"
    end

    test "shows error if user has no TOTP devices", %{conn: conn} do
      user_no_totp = user_fixture(%{email: "nototp@example.com"})

      conn = conn |> log_in_user(user_no_totp) |> get(~p"/users/sudo-verify")

      assert redirected_to(conn) == ~p"/account/totp-enrollment"
      assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "Two-factor authentication"
    end
  end

  describe "POST /users/sudo-verify" do
    test "redirects to login when not authenticated", %{conn: conn} do
      conn = post(conn, ~p"/users/sudo-verify", %{"user" => %{"totp_code" => "123456"}})
      assert redirected_to(conn) == ~p"/users/log-in"
    end

    test "grants sudo mode with valid TOTP code", %{conn: conn, user: user, device: device} do
      code = NimbleTOTP.verification_code(device.secret)

      conn =
        conn
        |> log_in_user(user)
        |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => code}})

      assert redirected_to(conn) == ~p"/users/settings"

      # Verify sudo mode was granted
      updated_user = Accounts.get_user!(user.id)
      assert Accounts.sudo_mode?(updated_user, -10)
    end

    test "redirects to return path after verification", %{conn: conn, user: user, device: device} do
      code = NimbleTOTP.verification_code(device.secret)

      conn =
        conn
        |> log_in_user(user)
        |> put_session(:user_return_to, "/custom/path")
        |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => code}})

      assert redirected_to(conn) == "/custom/path"
    end

    test "shows error with invalid TOTP code", %{conn: conn, user: user} do
      conn =
        conn
        |> log_in_user(user)
        |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => "000000"}})

      response = html_response(conn, 200)
      assert response =~ "Invalid verification code"
    end

    test "rejects recovery codes with specific error", %{conn: conn, user: user} do
      # Create a recovery code
      {:ok, codes} = Accounts.generate_recovery_codes(user)
      recovery_code = hd(codes)

      conn =
        conn
        |> log_in_user(user)
        |> post(~p"/users/sudo-verify", %{"user" => %{"totp_code" => recovery_code}})

      response = html_response(conn, 200)
      assert response =~ "Please use your authenticator app code"
      assert response =~ "not a recovery code"
    end
  end
end
```

**Step 2: Run test to verify it fails**

Run: `mix test test/towerops_web/controllers/user_sudo_controller_test.exs`

Expected: FAIL with "no route found" or "module not found"

**Step 3: Create UserSudoController**

Create `lib/towerops_web/controllers/user_sudo_controller.ex`:

```elixir
defmodule ToweropsWeb.UserSudoController do
  @moduledoc """
  Controller for sudo mode verification via TOTP.

  Provides a lightweight re-authentication flow that only requires TOTP
  instead of full email + password + TOTP login.
  """
  use ToweropsWeb, :controller

  alias Towerops.Accounts

  plug :require_authenticated_user

  @doc """
  Shows the TOTP verification form for sudo mode.

  If user is already in sudo mode, redirects to return path or settings.
  If user has no TOTP devices, redirects to enrollment.
  """
  def new(conn, _params) do
    user = conn.assigns.current_scope.user

    cond do
      # Already in sudo mode, redirect to destination
      Accounts.sudo_mode?(user, -10) ->
        redirect_to_return_path(conn)

      # No TOTP devices, must enroll first
      !Accounts.totp_enabled?(user) ->
        conn
        |> put_flash(:error, "Two-factor authentication is required for this action.")
        |> redirect(to: ~p"/account/totp-enrollment")

      # Show verification form
      true ->
        form = Phoenix.Component.to_form(%{}, as: "user")
        render(conn, :verify, form: form)
    end
  end

  @doc """
  Verifies the TOTP code and grants sudo mode.

  On success, redirects to return path or settings.
  On failure, re-renders form with error.
  """
  def verify(conn, %{"user" => %{"totp_code" => code}}) do
    user = conn.assigns.current_scope.user

    case Accounts.verify_totp_only(user, code) do
      {:ok, _user} ->
        # Grant sudo mode
        {:ok, _updated_user} = Accounts.grant_sudo_mode(user)

        conn
        |> put_flash(:info, "Identity verified successfully.")
        |> redirect_to_return_path()

      {:error, :recovery_code_not_allowed} ->
        form = Phoenix.Component.to_form(%{"totp_code" => code}, as: "user")

        conn
        |> put_flash(:error, "Please use your authenticator app code, not a recovery code.")
        |> render(:verify, form: form)

      {:error, _reason} ->
        form = Phoenix.Component.to_form(%{"totp_code" => code}, as: "user")

        conn
        |> put_flash(:error, "Invalid verification code. Please try again.")
        |> render(:verify, form: form)
    end
  end

  defp redirect_to_return_path(conn) do
    return_path = get_session(conn, :user_return_to) || ~p"/users/settings"
    conn = delete_session(conn, :user_return_to)
    redirect(conn, to: return_path)
  end

  defp require_authenticated_user(conn, _opts) do
    if conn.assigns.current_scope && conn.assigns.current_scope.user do
      conn
    else
      conn
      |> put_flash(:error, "You must log in to access this page.")
      |> redirect(to: ~p"/users/log-in")
      |> halt()
    end
  end
end
```

**Step 4: Create UserSudoHTML module**

Create `lib/towerops_web/controllers/user_sudo_html.ex`:

```elixir
defmodule ToweropsWeb.UserSudoHTML do
  @moduledoc """
  HTML templates for sudo mode verification.
  """
  use ToweropsWeb, :html

  embed_templates "user_sudo_html/*"
end
```

**Step 5: Create verify.html.heex template**

Create `lib/towerops_web/controllers/user_sudo_html/verify.html.heex`:

```heex
<div class="mx-auto max-w-sm">
  <.header class="text-center">
    Verify Your Identity
    <:subtitle>Enter the code from your authenticator app to continue</:subtitle>
  </.header>

  <.simple_form :let={f} for={@form} action={~p"/users/sudo-verify"} phx-update="ignore">
    <.error :if={@flash["error"]}>
      <%= @flash["error"] %>
    </.error>

    <.input
      field={f[:totp_code]}
      type="text"
      label="Verification Code"
      placeholder="000000"
      required
      autofocus
      autocomplete="off"
      inputmode="numeric"
      pattern="[0-9]*"
      maxlength="6"
    />

    <:actions>
      <.button phx-disable-with="Verifying..." class="w-full">
        Verify
      </.button>
    </:actions>
  </.simple_form>

  <p class="text-center text-sm text-zinc-500 mt-4">
    Need help? <.link href={~p"/users/settings"} class="text-brand hover:underline">Go back</.link>
  </p>
</div>
```

**Step 6: Add routes to router**

In `lib/towerops_web/router.ex`, find the scope with `pipe_through [:browser, :require_authenticated_user]` and add:

```elixir
get "/users/sudo-verify", UserSudoController, :new
post "/users/sudo-verify", UserSudoController, :verify
```

**Step 7: Run test to verify it passes**

Run: `mix test test/towerops_web/controllers/user_sudo_controller_test.exs`

Expected: PASS (10 tests)

**Step 8: Commit**

```bash
git add lib/towerops_web/controllers/user_sudo_controller.ex lib/towerops_web/controllers/user_sudo_html.ex lib/towerops_web/controllers/user_sudo_html/verify.html.heex lib/towerops_web/router.ex test/towerops_web/controllers/user_sudo_controller_test.exs
git commit -m "feat: add UserSudoController for TOTP-only verification"
```

---

## Task 4: Update UserAuth plugs to redirect to sudo verify

**Files:**
- Modify: `lib/towerops_web/user_auth.ex`
- Modify: `test/towerops_web/user_auth_test.exs`

**Step 1: Write failing tests for updated behavior**

In `test/towerops_web/user_auth_test.exs`, find the `describe "require_sudo_mode/2"` block and update it:

```elixir
describe "require_sudo_mode/2" do
  test "redirects to sudo verify when sudo mode expired and TOTP enabled", %{conn: conn} do
    user = user_fixture()

    # Create TOTP device
    {:ok, _device} =
      Accounts.create_user_totp_device(user, %{
        name: "Test Device",
        secret: NimbleTOTP.secret()
      })

    conn =
      conn
      |> log_in_user(user)
      |> UserAuth.require_sudo_mode([])

    assert redirected_to(conn) == ~p"/users/sudo-verify"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "verify your identity"
  end

  test "redirects to TOTP enrollment when sudo mode expired and no TOTP", %{conn: conn} do
    user = user_fixture()

    conn =
      conn
      |> log_in_user(user)
      |> UserAuth.require_sudo_mode([])

    assert redirected_to(conn) == ~p"/account/totp-enrollment"
    assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "Two-factor authentication"
  end

  test "allows access when sudo mode is active", %{conn: conn} do
    user = user_fixture()
    {:ok, user} = Accounts.grant_sudo_mode(user)

    conn =
      conn
      |> log_in_user(user)
      |> UserAuth.require_sudo_mode([])

    refute conn.halted
  end
end
```

**Step 2: Run test to verify it fails**

Run: `mix test test/towerops_web/user_auth_test.exs::<line_number>`

(Replace `<line_number>` with the line number of the describe block)

Expected: FAIL - tests expect old redirect behavior

**Step 3: Update require_sudo_mode/2 plug**

In `lib/towerops_web/user_auth.ex`, replace the `require_sudo_mode/2` function:

```elixir
@doc """
Plug for routes that require sudo mode.

Checks if the user's sudo mode is active (within 10 minutes).
If TOTP is enabled, redirects to TOTP-only verification page.
If TOTP is not enabled, redirects to enrollment.
"""
def require_sudo_mode(conn, _opts) do
  user = conn.assigns.current_scope.user

  case {Accounts.sudo_mode?(user, -10), Accounts.totp_enabled?(user)} do
    {true, _} ->
      conn

    {false, true} ->
      conn
      |> put_flash(:error, "Please verify your identity to continue.")
      |> maybe_store_return_to()
      |> redirect(to: ~p"/users/sudo-verify")
      |> halt()

    {false, false} ->
      conn
      |> put_flash(:error, "Two-factor authentication is required for this action.")
      |> redirect(to: ~p"/account/totp-enrollment")
      |> halt()
  end
end
```

**Step 4: Run test to verify it passes**

Run: `mix test test/towerops_web/user_auth_test.exs`

Expected: PASS for require_sudo_mode tests

**Step 5: Write failing tests for on_mount hook**

In `test/towerops_web/user_auth_test.exs`, find or create the `describe "on_mount: :require_sudo_mode"` block:

```elixir
describe "on_mount: :require_sudo_mode" do
  test "redirects to sudo verify when sudo mode expired and TOTP enabled" do
    user = user_fixture()

    # Create TOTP device
    {:ok, _device} =
      Accounts.create_user_totp_device(user, %{
        name: "Test Device",
        secret: NimbleTOTP.secret()
      })

    socket = %Phoenix.LiveView.Socket{}
    socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))

    assert {:halt, redirected_socket} =
      UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)

    assert Phoenix.Flash.get(redirected_socket.assigns.flash, :error) =~ "verify your identity"
  end

  test "redirects to TOTP enrollment when sudo mode expired and no TOTP" do
    user = user_fixture()

    socket = %Phoenix.LiveView.Socket{}
    socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))

    assert {:halt, redirected_socket} =
      UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)

    assert Phoenix.Flash.get(redirected_socket.assigns.flash, :error) =~ "Two-factor authentication"
  end

  test "allows access when sudo mode is active" do
    user = user_fixture()
    {:ok, user} = Accounts.grant_sudo_mode(user)

    socket = %Phoenix.LiveView.Socket{}
    socket = Phoenix.Component.assign(socket, :current_scope, Scope.for_user(user))

    assert {:cont, _socket} = UserAuth.on_mount(:require_sudo_mode, %{}, %{}, socket)
  end
end
```

**Step 6: Run test to verify it fails**

Run: `mix test test/towerops_web/user_auth_test.exs`

Expected: FAIL for on_mount tests

**Step 7: Update on_mount(:require_sudo_mode) hook**

In `lib/towerops_web/user_auth.ex`, replace the `on_mount(:require_sudo_mode)` clause:

```elixir
def on_mount(:require_sudo_mode, _params, _session, socket) do
  user = socket.assigns.current_scope && socket.assigns.current_scope.user

  case {user && Accounts.sudo_mode?(user, -10), user && Accounts.totp_enabled?(user)} do
    {true, _} ->
      {:cont, socket}

    {false, true} ->
      socket =
        socket
        |> LiveView.put_flash(:error, "Please verify your identity to continue.")
        |> LiveView.redirect(to: ~p"/users/sudo-verify")

      {:halt, socket}

    {false, false} ->
      socket =
        socket
        |> LiveView.put_flash(:error, "Two-factor authentication is required for this action.")
        |> LiveView.redirect(to: ~p"/account/totp-enrollment")

      {:halt, socket}

    _ ->
      {:halt, LiveView.redirect(socket, to: ~p"/users/log-in")}
  end
end
```

**Step 8: Run test to verify all tests pass**

Run: `mix test test/towerops_web/user_auth_test.exs`

Expected: PASS for all UserAuth tests

**Step 9: Commit**

```bash
git add lib/towerops_web/user_auth.ex test/towerops_web/user_auth_test.exs
git commit -m "feat: update UserAuth to redirect to sudo verify page"
```

---

## Task 5: Update UserSettingsLive tests

**Files:**
- Modify: `test/towerops_web/live/user_settings_live_test.exs`

**Step 1: Add helper to grant sudo mode in setup**

In `test/towerops_web/live/user_settings_live_test.exs`, update the setup block:

```elixir
setup %{conn: conn} do
  user = user_fixture()

  # Grant sudo mode so tests can access settings page
  {:ok, user} = Accounts.grant_sudo_mode(user)

  %{conn: log_in_user(conn, user), user: user}
end
```

**Step 2: Add test for sudo mode redirect**

Add a new describe block in the same file:

```elixir
describe "sudo mode requirement" do
  test "redirects to sudo verify when sudo mode expired", %{conn: conn} do
    user = user_fixture()
    # Don't grant sudo mode

    {:error, {:redirect, %{to: path}}} =
      live(log_in_user(conn, user), ~p"/users/settings")

    assert path == "/users/sudo-verify"
  end

  test "allows access when sudo mode is active", %{conn: conn, user: user} do
    # Sudo mode already granted in setup
    {:ok, _view, html} = live(conn, ~p"/users/settings")

    assert html =~ "Account Settings"
  end
end
```

**Step 3: Run tests to verify they pass**

Run: `mix test test/towerops_web/live/user_settings_live_test.exs`

Expected: PASS for all UserSettingsLive tests

**Step 4: Commit**

```bash
git add test/towerops_web/live/user_settings_live_test.exs
git commit -m "test: update UserSettingsLive tests for sudo mode"
```

---

## Task 6: Run full test suite and format

**Step 1: Run all tests**

Run: `mix test`

Expected: All tests pass

**Step 2: Format code**

Run: `mix format`

Expected: All files formatted

**Step 3: Run precommit checks**

Run: `mix precommit`

Expected: All checks pass (compile, format, test)

**Step 4: Commit if any formatting changes**

```bash
git add -u
git commit -m "chore: format code"
```

---

## Task 7: Manual testing

**Step 1: Start the server**

Run: `mix phx.server`

**Step 2: Test the flow**

1. Log in as a user with TOTP enabled
2. Navigate to `/users/settings`
3. Should redirect to `/users/sudo-verify` (if sudo mode expired)
4. Enter valid TOTP code
5. Should redirect back to `/users/settings`
6. Verify you can access settings without re-verifying for 10 minutes

**Step 3: Test error cases**

1. Try entering invalid TOTP code - should show error
2. Try entering recovery code - should show specific error message
3. Navigate to `/users/sudo-verify` while already in sudo mode - should redirect to settings

**Step 4: Document any issues found**

If any issues are found, create new tasks to fix them.

---

## Completion Checklist

- [ ] Task 1: Add Accounts.grant_sudo_mode/1 function
- [ ] Task 2: Add Accounts.verify_totp_only/2 function
- [ ] Task 3: Create UserSudoController and HTML module
- [ ] Task 4: Update UserAuth plugs to redirect to sudo verify
- [ ] Task 5: Update UserSettingsLive tests
- [ ] Task 6: Run full test suite and format
- [ ] Task 7: Manual testing

## Success Criteria

✅ All tests pass
✅ Code formatted and precommit checks pass
✅ User can verify sudo mode with TOTP only (no password)
✅ Recovery codes are rejected with clear error message
✅ Return path redirection works correctly
✅ Edge cases handled gracefully
