Merge auth links into main nav and lower password min to 8

- Register/Log in (or callsign/Settings/Log out) now live next to
  the Map/Path/Rover links in the main header instead of a separate
  top menu bar
- Add on_mount in UserAuth to assign current_scope on LiveView mount,
  and pass current_scope through to Layouts.app from every LiveView
- Drop the old top <ul> from root.html.heex
- Password minimum lowered from 12 to 8 characters
This commit is contained in:
Graham McIntire 2026-04-08 10:39:30 -05:00
parent adaa17719a
commit a8995bb43b
13 changed files with 50 additions and 34 deletions

View file

@ -131,7 +131,7 @@ defmodule Microwaveprop.Accounts.User do
defp validate_password(changeset, opts) do defp validate_password(changeset, opts) do
changeset changeset
|> validate_required([:password]) |> validate_required([:password])
|> validate_length(:password, min: 12, max: 72) |> validate_length(:password, min: 8, max: 72)
# Examples of additional password validation: # Examples of additional password validation:
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character") # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character") # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")

View file

@ -52,6 +52,8 @@ defmodule MicrowavepropWeb do
quote do quote do
use Phoenix.LiveView use Phoenix.LiveView
on_mount MicrowavepropWeb.UserAuth
unquote(html_helpers()) unquote(html_helpers())
end end
end end

View file

@ -46,6 +46,18 @@ defmodule MicrowavepropWeb.Layouts do
<.link navigate="/contacts/map" class="btn btn-ghost btn-sm">Contact Map</.link> <.link navigate="/contacts/map" class="btn btn-ghost btn-sm">Contact Map</.link>
<.link navigate="/contacts" class="btn btn-ghost btn-sm">Contacts</.link> <.link navigate="/contacts" class="btn btn-ghost btn-sm">Contacts</.link>
<.link navigate="/submit" class="btn btn-ghost btn-sm">Submit</.link> <.link navigate="/submit" class="btn btn-ghost btn-sm">Submit</.link>
<%= if @current_scope && @current_scope.user do %>
<span class="btn btn-ghost btn-sm pointer-events-none opacity-70">
{@current_scope.user.callsign}
</span>
<.link href={~p"/users/settings"} class="btn btn-ghost btn-sm">Settings</.link>
<.link href={~p"/users/log-out"} method="delete" class="btn btn-ghost btn-sm">
Log out
</.link>
<% else %>
<.link href={~p"/users/register"} class="btn btn-ghost btn-sm">Register</.link>
<.link href={~p"/users/log-in"} class="btn btn-ghost btn-sm">Log in</.link>
<% end %>
</nav> </nav>
</div> </div>
<div class="flex-none"> <div class="flex-none">

View file

@ -37,26 +37,6 @@
</script> </script>
</head> </head>
<body> <body>
<ul class="menu menu-horizontal w-full relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_scope do %>
<li>
{@current_scope.user.email}
</li>
<li>
<.link href={~p"/users/settings"}>Settings</.link>
</li>
<li>
<.link href={~p"/users/log-out"} method="delete">Log out</.link>
</li>
<% else %>
<li>
<.link href={~p"/users/register"}>Register</.link>
</li>
<li>
<.link href={~p"/users/log-in"}>Log in</.link>
</li>
<% end %>
</ul>
{@inner_content} {@inner_content}
</body> </body>
</html> </html>

View file

@ -13,7 +13,7 @@ defmodule MicrowavepropWeb.AlgoLive do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash}> <Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="markdown-content"> <div class="markdown-content">
{raw(@content)} {raw(@content)}
</div> </div>

View file

@ -311,7 +311,7 @@ defmodule MicrowavepropWeb.BackfillLive do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash}> <Layouts.app flash={@flash} current_scope={@current_scope}>
<.header> <.header>
Backfill Dashboard Backfill Dashboard
<:subtitle>Enqueue and monitor contact enrichment jobs</:subtitle> <:subtitle>Enqueue and monitor contact enrichment jobs</:subtitle>

View file

@ -112,7 +112,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash} max_width="max-w-7xl"> <Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-7xl">
<.header> <.header>
Contacts Contacts
<:subtitle>{@total_entries} contacts</:subtitle> <:subtitle>{@total_entries} contacts</:subtitle>

View file

@ -435,7 +435,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash}> <Layouts.app flash={@flash} current_scope={@current_scope}>
<.header> <.header>
<span class={[@contact.flagged_invalid && "line-through opacity-50"]}> <span class={[@contact.flagged_invalid && "line-through opacity-50"]}>
{@contact.station1} / {@contact.station2} {@contact.station1} / {@contact.station2}

View file

@ -466,7 +466,7 @@ defmodule MicrowavepropWeb.PathLive do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash} max_width="max-w-6xl"> <Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-6xl">
<.header> <.header>
Path Calculator Path Calculator
<:subtitle>Analyze microwave propagation between two points</:subtitle> <:subtitle>Analyze microwave propagation between two points</:subtitle>

View file

@ -127,7 +127,7 @@ defmodule MicrowavepropWeb.SubmitLive do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<Layouts.app flash={@flash}> <Layouts.app flash={@flash} current_scope={@current_scope}>
<.header> <.header>
Submit Contact Submit Contact
<:subtitle>Help us build a better propagation model</:subtitle> <:subtitle>Help us build a better propagation model</:subtitle>

View file

@ -18,6 +18,28 @@ defmodule MicrowavepropWeb.UserAuth do
same_site: "Lax" same_site: "Lax"
] ]
@doc """
LiveView `on_mount` callback that assigns `:current_scope` from the
session token so LiveView templates can show auth-aware navigation.
"""
def on_mount(:default, _params, session, socket) do
{:cont, mount_current_scope(socket, session)}
end
defp mount_current_scope(socket, session) do
Phoenix.Component.assign_new(socket, :current_scope, fn ->
user =
with token when is_binary(token) <- session["user_token"],
{user, _inserted_at} <- Accounts.get_user_by_session_token(token) do
user
else
_ -> nil
end
Scope.for_user(user)
end)
end
# How old the session token should be before a new one is issued. When a request is made # How old the session token should be before a new one is issued. When a request is made
# with a session token older than this value, then a new session token will be created # with a session token older than this value, then a new session token will be created
# and the session and remember-me cookies (if set) will be updated with the new token. # and the session and remember-me cookies (if set) will be updated with the new token.

View file

@ -120,7 +120,7 @@ defmodule Microwaveprop.AccountsTest do
{:error, changeset} = {:error, changeset} =
Accounts.register_user(valid_user_attributes(password: "short")) Accounts.register_user(valid_user_attributes(password: "short"))
assert "should be at least 12 character(s)" in errors_on(changeset).password assert "should be at least 8 character(s)" in errors_on(changeset).password
end end
test "registers a valid user but leaves them unconfirmed" do test "registers a valid user but leaves them unconfirmed" do
@ -323,12 +323,12 @@ defmodule Microwaveprop.AccountsTest do
test "validates password", %{user: user} do test "validates password", %{user: user} do
{:error, changeset} = {:error, changeset} =
Accounts.update_user_password(user, %{ Accounts.update_user_password(user, %{
password: "not valid", password: "short",
password_confirmation: "another" password_confirmation: "other"
}) })
assert %{ assert %{
password: ["should be at least 12 character(s)"], password: ["should be at least 8 character(s)"],
password_confirmation: ["does not match password"] password_confirmation: ["does not match password"]
} = errors_on(changeset) } = errors_on(changeset)
end end

View file

@ -56,14 +56,14 @@ defmodule MicrowavepropWeb.UserSettingsControllerTest do
put(conn, ~p"/users/settings", %{ put(conn, ~p"/users/settings", %{
"action" => "update_password", "action" => "update_password",
"user" => %{ "user" => %{
"password" => "too short", "password" => "short",
"password_confirmation" => "does not match" "password_confirmation" => "nope"
} }
}) })
response = html_response(old_password_conn, 200) response = html_response(old_password_conn, 200)
assert response =~ "Settings" assert response =~ "Settings"
assert response =~ "should be at least 12 character(s)" assert response =~ "should be at least 8 character(s)"
assert response =~ "does not match password" assert response =~ "does not match password"
assert get_session(old_password_conn, :user_token) == get_session(conn, :user_token) assert get_session(old_password_conn, :user_token) == get_session(conn, :user_token)