From e46ecbfca2f7b61ac00e400b5de0c88661c2bc24 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 1 Feb 2026 15:00:23 -0600 Subject: [PATCH] Replace single name field with first_name and last_name --- lib/towerops/accounts/user.ex | 32 ++++- .../api/account_data_controller.ex | 3 +- lib/towerops_web/live/account_live/my_data.ex | 13 +- .../live/user_registration_live.ex | 16 ++- lib/towerops_web/live/user_settings_live.ex | 36 ++++- ...201204935_add_first_last_name_to_users.exs | 17 +++ test/towerops/accounts/user_test.exs | 126 ++++++++++++++++++ 7 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 priv/repo/migrations/20260201204935_add_first_last_name_to_users.exs create mode 100644 test/towerops/accounts/user_test.exs diff --git a/lib/towerops/accounts/user.ex b/lib/towerops/accounts/user.ex index 5fd5f5a1..b55f68e7 100644 --- a/lib/towerops/accounts/user.ex +++ b/lib/towerops/accounts/user.ex @@ -23,7 +23,8 @@ defmodule Towerops.Accounts.User do field :confirmed_at, :utc_datetime field :authenticated_at, :utc_datetime, virtual: true field :is_superuser, :boolean, default: false - field :name, :string + field :first_name, :string + field :last_name, :string field :timezone, :string, default: "UTC" field :totp_secret, :binary, redact: true field :totp_verified_at, :utc_datetime, virtual: true @@ -46,7 +47,8 @@ defmodule Towerops.Accounts.User do confirmed_at: DateTime.t() | nil, authenticated_at: DateTime.t() | nil, is_superuser: boolean(), - name: String.t() | nil, + first_name: String.t() | nil, + last_name: String.t() | nil, timezone: String.t(), totp_secret: binary() | nil, totp_verified_at: DateTime.t() | nil, @@ -100,12 +102,13 @@ defmodule Towerops.Accounts.User do end @doc """ - A user changeset for updating the profile information (name, timezone). + A user changeset for updating the profile information (first_name, last_name, timezone). """ def profile_changeset(user, attrs) do user - |> cast(attrs, [:name, :timezone]) - |> validate_length(:name, max: 255) + |> cast(attrs, [:first_name, :last_name, :timezone]) + |> validate_length(:first_name, max: 100) + |> validate_length(:last_name, max: 100) |> validate_length(:timezone, max: 100) end @@ -124,11 +127,13 @@ defmodule Towerops.Accounts.User do """ def registration_changeset(user, attrs, opts \\ []) do user - |> cast(attrs, [:email, :password, :timezone, :privacy_policy_consent, :terms_of_service_consent]) + |> cast(attrs, [:email, :password, :first_name, :last_name, :timezone, :privacy_policy_consent, :terms_of_service_consent]) |> validate_email_for_registration(opts) |> validate_password(opts) |> validate_consent() |> validate_timezone() + |> validate_length(:first_name, max: 100) + |> validate_length(:last_name, max: 100) end defp validate_consent(changeset) do @@ -255,4 +260,19 @@ defmodule Towerops.Accounts.User do Argon2.no_user_verify() false end + + @doc """ + Returns the user's full name by combining first_name and last_name. + + Returns nil if both fields are nil or empty. + """ + @spec full_name(t()) :: String.t() | nil + def full_name(%__MODULE__{first_name: first, last_name: last}) do + [first, last] + |> Enum.reject(&(is_nil(&1) or &1 == "")) + |> case do + [] -> nil + parts -> Enum.join(parts, " ") + end + end end diff --git a/lib/towerops_web/controllers/api/account_data_controller.ex b/lib/towerops_web/controllers/api/account_data_controller.ex index 2ee7cfe9..4fa2431b 100644 --- a/lib/towerops_web/controllers/api/account_data_controller.ex +++ b/lib/towerops_web/controllers/api/account_data_controller.ex @@ -47,7 +47,8 @@ defmodule ToweropsWeb.Api.AccountDataController do %{ id: user.id, email: user.email, - name: user.name, + first_name: user.first_name, + last_name: user.last_name, timezone: user.timezone, is_superuser: user.is_superuser, confirmed_at: user.confirmed_at, diff --git a/lib/towerops_web/live/account_live/my_data.ex b/lib/towerops_web/live/account_live/my_data.ex index b5eccec6..431673cd 100644 --- a/lib/towerops_web/live/account_live/my_data.ex +++ b/lib/towerops_web/live/account_live/my_data.ex @@ -166,9 +166,15 @@ defmodule ToweropsWeb.AccountLive.MyData do
{@user_data.profile.email}
-
Name
+
First Name
- {@user_data.profile.name || "Not set"} + {@user_data.profile.first_name || "Not set"} +
+
+
+
Last Name
+
+ {@user_data.profile.last_name || "Not set"}
@@ -564,7 +570,8 @@ defmodule ToweropsWeb.AccountLive.MyData do defp get_user_profile(user) do %{ email: user.email, - name: user.name, + first_name: user.first_name, + last_name: user.last_name, timezone: user.timezone, inserted_at: user.inserted_at, confirmed_at: user.confirmed_at diff --git a/lib/towerops_web/live/user_registration_live.ex b/lib/towerops_web/live/user_registration_live.ex index 79ef06b2..20fa686e 100644 --- a/lib/towerops_web/live/user_registration_live.ex +++ b/lib/towerops_web/live/user_registration_live.ex @@ -167,13 +167,27 @@ defmodule ToweropsWeb.UserRegistrationLive do <% end %> +
+ <.input + field={@form[:first_name]} + type="text" + label="First name" + autocomplete="given-name" + phx-mounted={JS.focus()} + /> + <.input + field={@form[:last_name]} + type="text" + label="Last name" + autocomplete="family-name" + /> +
<.input field={@form[:email]} type="email" label="Email" autocomplete="email" required - phx-mounted={JS.focus()} /> <.input field={@form[:password]} diff --git a/lib/towerops_web/live/user_settings_live.ex b/lib/towerops_web/live/user_settings_live.ex index 99d21001..bd80c215 100644 --- a/lib/towerops_web/live/user_settings_live.ex +++ b/lib/towerops_web/live/user_settings_live.ex @@ -759,17 +759,39 @@ defmodule ToweropsWeb.UserSettingsLive do class="md:col-span-2" >
-
-