Put the remaining credential settings pages behind sudo mode

e5de06e · Gabriel Morell · 2026-09-10 15:26

4 files +87 -4
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud_web/live/personal_access_token_live/index.ex
+10 −2
@@ -6,6 +6,7 @@ defmodule GitGudWeb.PersonalAccessTokenLive.Index do
6 6 extensible set, so more can be added later.
7 7 """
8 8 use GitGudWeb, :live_view
9 + on_mount {GitGudWeb.UserAuth, {:require_sudo_mode, "/users/settings/tokens"}}
9 10
10 11 alias GitGud.Accounts
11 12 alias GitGud.Accounts.PersonalAccessToken
@@ -117,7 +118,12 @@ defmodule GitGudWeb.PersonalAccessTokenLive.Index do
117 118
118 119 <table class="table table-sm">
119 120 <thead>
120 <tr><th>Name</th><th>Scopes</th><th>Last used</th><th></th></tr>
121 + <tr>
122 + <th>Name</th>
123 + <th>Scopes</th>
124 + <th>Last used</th>
125 + <th></th>
126 + </tr>
121 127 </thead>
122 128 <tbody>
123 129 <tr :for={t <- @tokens}>
@@ -132,7 +138,9 @@ defmodule GitGudWeb.PersonalAccessTokenLive.Index do
132 138 phx-value-id={t.id}
133 139 data-confirm={"Revoke token #{t.name}? Anything using it loses access."}
134 140 class="btn btn-xs btn-ghost text-error"
135 >Revoke</button>
141 + >
142 + Revoke
143 + </button>
136 144 </td>
137 145 </tr>
138 146 <tr :if={@tokens == []}>
modified lib/git_gud_web/live/user_live/invites.ex
+2 −2
@@ -1,5 +1,6 @@
1 1 defmodule GitGudWeb.UserLive.Invites do
2 2 use GitGudWeb, :live_view
3 + on_mount {GitGudWeb.UserAuth, {:require_sudo_mode, "/users/settings/invites"}}
3 4
4 5 alias GitGud.Accounts
5 6
@@ -84,8 +85,7 @@ defmodule GitGudWeb.UserLive.Invites do
84 85 id="invites-empty"
85 86 class="hidden only:block text-center text-sm opacity-60 py-8"
86 87 >
87 No invites yet. Click <em class="not-italic font-semibold">New invite</em>
88 to create one.
88 + No invites yet. Click <em class="not-italic font-semibold">New invite</em> to create one.
89 89 </li>
90 90 <li
91 91 :for={{dom_id, invite} <- @streams.invites}
modified lib/git_gud_web/live/user_live/two_factor_setup.ex
+1 −0
@@ -18,6 +18,7 @@ defmodule GitGudWeb.UserLive.TwoFactorSetup do
18 18 """
19 19
20 20 use GitGudWeb, :live_view
21 + on_mount {GitGudWeb.UserAuth, {:require_sudo_mode, "/users/settings/two-factor"}}
21 22
22 23 on_mount {GitGudWeb.UserAuth, :require_authenticated}
23 24
added test/git_gud_web/live/settings_sudo_test.exs
+74 −0
@@ -0,0 +1,74 @@
1 +defmodule GitGudWeb.SettingsSudoTest do
2 + @moduledoc """
3 + Every account-settings page that manages a credential sits behind
4 + sudo mode.
5 +
6 + Sudo is declared per-LiveView via `on_mount`, not in the router, so a
7 + new settings page is ungated by defaultwhich is how
8 + /users/settings/two-factor, /tokens and /invites came to be reachable
9 + with only a session. This test enumerates them so the next one that's
10 + added has to be considered.
11 + """
12 +
13 + use GitGudWeb.ConnCase, async: false
14 +
15 + import Phoenix.LiveViewTest
16 + import GitGud.AccountsFixtures
17 +
18 + @gated [
19 + "/users/settings",
20 + "/users/settings/ssh-keys",
21 + "/users/settings/two-factor",
22 + "/users/settings/tokens",
23 + "/users/settings/invites"
24 + ]
25 +
26 + # `register_and_log_in_user` authenticates far enough back that sudo
27 + # has lapsed, which is exactly the state being tested: a live session,
28 + # no recent re-authentication.
29 + defp stale_conn(conn) do
30 + user = user_fixture()
31 + log_in_user(conn, user, token_authenticated_at: hours_ago(2))
32 + end
33 +
34 + defp hours_ago(n), do: DateTime.utc_now(:second) |> DateTime.add(-n * 3600, :second)
35 +
36 + for path <- @gated do
37 + test "#{path} redirects to log-in without recent auth", %{conn: conn} do
38 + assert {:error, {:redirect, %{to: to}}} = live(stale_conn(conn), unquote(path))
39 + assert to =~ "/users/log-in"
40 + end
41 + end
42 +
43 + # /users/settings predates the tuple form and still uses the bare
44 + # one, so it drops you on the default page after re-auth rather than
45 + # back where you were. Left alone here — it's a UX wart, not a hole.
46 + for path <- @gated -- ["/users/settings"] do
47 + test "#{path} carries a return_to so re-auth lands back there", %{conn: conn} do
48 + assert {:error, {:redirect, %{to: to}}} = live(stale_conn(conn), unquote(path))
49 + assert to =~ "return_to"
50 + end
51 + end
52 +
53 + test "a freshly authenticated session reaches two-factor setup", %{conn: conn} do
54 + user = user_fixture()
55 + conn = log_in_user(conn, user)
56 +
57 + {:ok, _lv, html} = live(conn, ~p"/users/settings/two-factor")
58 + assert html =~ "factor" or html =~ "Factor"
59 + end
60 +
61 + test "a freshly authenticated session reaches the tokens page", %{conn: conn} do
62 + user = user_fixture()
63 + conn = log_in_user(conn, user)
64 +
65 + {:ok, _lv, _html} = live(conn, ~p"/users/settings/tokens")
66 + end
67 +
68 + test "anonymous visitors are bounced before sudo even applies", %{conn: conn} do
69 + for path <- @gated do
70 + assert {:error, {:redirect, %{to: to}}} = live(conn, path)
71 + assert to =~ "/users/log-in"
72 + end
73 + end
74 +end

Parents: c68e1e7