diff --git a/lib/towerops/accounts.ex b/lib/towerops/accounts.ex index 853607bc..f580072f 100644 --- a/lib/towerops/accounts.ex +++ b/lib/towerops/accounts.ex @@ -139,6 +139,37 @@ defmodule Towerops.Accounts do ## Settings + @doc """ + Returns an `%Ecto.Changeset{}` for changing the user profile. + + ## Examples + + iex> change_user_profile(user) + %Ecto.Changeset{data: %User{}} + + """ + def change_user_profile(user, attrs \\ %{}) do + User.profile_changeset(user, attrs) + end + + @doc """ + Updates the user profile. + + ## Examples + + iex> update_user_profile(user, %{name: ...}) + {:ok, %User{}} + + iex> update_user_profile(user, %{name: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_user_profile(user, attrs) do + user + |> User.profile_changeset(attrs) + |> Repo.update() + end + @doc """ Checks whether the user is in sudo mode. diff --git a/lib/towerops/accounts/user.ex b/lib/towerops/accounts/user.ex index 6e6a954d..4857decb 100644 --- a/lib/towerops/accounts/user.ex +++ b/lib/towerops/accounts/user.ex @@ -24,6 +24,9 @@ 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 :avatar_url, :string + field :timezone, :string has_many :memberships, Membership has_many :organizations, through: [:memberships, :organization] @@ -40,6 +43,9 @@ defmodule Towerops.Accounts.User do confirmed_at: DateTime.t() | nil, authenticated_at: DateTime.t() | nil, is_superuser: boolean(), + name: String.t() | nil, + avatar_url: String.t() | nil, + timezone: String.t(), memberships: NotLoaded.t() | [Membership.t()], organizations: NotLoaded.t() | [Towerops.Organizations.Organization.t()], credentials: NotLoaded.t() | [UserCredential.t()], @@ -89,6 +95,17 @@ defmodule Towerops.Accounts.User do end end + @doc """ + A user changeset for updating the profile information (name, avatar_url, timezone). + """ + def profile_changeset(user, attrs) do + user + |> cast(attrs, [:name, :avatar_url, :timezone]) + |> validate_length(:name, max: 255) + |> validate_length(:avatar_url, max: 500) + |> validate_length(:timezone, max: 100) + end + @doc """ A user changeset for changing the password. diff --git a/lib/towerops_web/live/user_settings_live.ex b/lib/towerops_web/live/user_settings_live.ex index a007b570..b73dbc03 100644 --- a/lib/towerops_web/live/user_settings_live.ex +++ b/lib/towerops_web/live/user_settings_live.ex @@ -15,6 +15,7 @@ defmodule ToweropsWeb.UserSettingsLive do socket |> assign(:page_title, "Account Settings") |> assign_changesets() + |> assign_profile_form() |> assign_credentials() |> assign_mobile_sessions() |> assign_api_tokens() @@ -26,6 +27,24 @@ defmodule ToweropsWeb.UserSettingsLive do {:ok, socket} end + @impl true + def handle_event("update_profile", %{"user" => user_params}, socket) do + user = socket.assigns.current_scope.user + + case Accounts.update_user_profile(user, user_params) do + {:ok, _updated_user} -> + socket = + socket + |> put_flash(:info, "Profile updated successfully.") + |> assign_profile_form() + + {:noreply, socket} + + {:error, changeset} -> + {:noreply, assign(socket, :profile_form, to_form(changeset))} + end + end + @impl true def handle_event("update_email", %{"user" => user_params}, socket) do user = socket.assigns.current_scope.user @@ -162,6 +181,11 @@ defmodule ToweropsWeb.UserSettingsLive do end end + defp assign_profile_form(socket) do + user = socket.assigns.current_scope.user + assign(socket, :profile_form, user |> Accounts.change_user_profile() |> to_form()) + end + defp assign_changesets(socket) do user = socket.assigns.current_scope.user @@ -217,10 +241,172 @@ defmodule ToweropsWeb.UserSettingsLive do
- +
-

Email Address

+

+ Personal Information +

+

+ Update your name, avatar, and timezone preferences. +

+
+ + <.form + for={@profile_form} + phx-submit="update_profile" + id="update_profile" + class="md:col-span-2" + > +
+
+ +
+ +
+
+ +
+ +
+ +
+

+ Enter a URL to an avatar image or use a service like Gravatar. +

+
+ +
+ +
+ + +
+
+
+ +
+ +
+ +
+ + +
+
+

Account - Email

Update your email address. You'll receive a confirmation link to verify the new address.

@@ -263,10 +449,12 @@ defmodule ToweropsWeb.UserSettingsLive do
- +
-

Change Password

+

+ Account - Password +

Update your password. You'll be logged out and need to sign in again with your new password.

@@ -408,11 +596,11 @@ defmodule ToweropsWeb.UserSettingsLive do
- +

- Alert Notification Devices + Notifications - Mobile Devices

Manage mobile devices that receive push notifications for alerts. @@ -501,10 +689,12 @@ defmodule ToweropsWeb.UserSettingsLive do

- +
-

Passkeys

+

+ Security - Passkeys +

Use your device's biometrics (Face ID, Touch ID, Windows Hello) or security keys to sign in.

diff --git a/priv/repo/migrations/20260118162909_add_profile_fields_to_users.exs b/priv/repo/migrations/20260118162909_add_profile_fields_to_users.exs new file mode 100644 index 00000000..11345fe4 --- /dev/null +++ b/priv/repo/migrations/20260118162909_add_profile_fields_to_users.exs @@ -0,0 +1,11 @@ +defmodule Towerops.Repo.Migrations.AddProfileFieldsToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add :name, :string + add :avatar_url, :string + add :timezone, :string, default: "UTC" + end + end +end