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 f75d0e7f54
commit 27170b5139
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
changeset
|> validate_required([:password])
|> validate_length(:password, min: 12, max: 72)
|> validate_length(:password, min: 8, max: 72)
# 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 upper case character")

View file

@ -52,6 +52,8 @@ defmodule MicrowavepropWeb do
quote do
use Phoenix.LiveView
on_mount MicrowavepropWeb.UserAuth
unquote(html_helpers())
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" class="btn btn-ghost btn-sm">Contacts</.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>
</div>
<div class="flex-none">

View file

@ -37,26 +37,6 @@
</script>
</head>
<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}
</body>
</html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,28 @@ defmodule MicrowavepropWeb.UserAuth do
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
# 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.

View file

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

View file

@ -56,14 +56,14 @@ defmodule MicrowavepropWeb.UserSettingsControllerTest do
put(conn, ~p"/users/settings", %{
"action" => "update_password",
"user" => %{
"password" => "too short",
"password_confirmation" => "does not match"
"password" => "short",
"password_confirmation" => "nope"
}
})
response = html_response(old_password_conn, 200)
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 get_session(old_password_conn, :user_token) == get_session(conn, :user_token)