defmodule ToweropsWeb.UserSettingsLive do @moduledoc """ LiveView for managing user account settings. Allows users to update email, password, manage passkeys, and mobile sessions. """ use ToweropsWeb, :live_view alias Towerops.Accounts alias Towerops.MobileSessions @impl true def mount(_params, _session, socket) do socket = socket |> assign(:page_title, "Account Settings") |> assign_changesets() |> assign_credentials() |> assign_mobile_sessions() {:ok, socket} end @impl true def handle_event("update_email", %{"user" => user_params}, socket) do user = socket.assigns.current_scope.user case Accounts.change_user_email(user, user_params) do %{valid?: true} = changeset -> _ = Accounts.deliver_user_update_email_instructions( Ecto.Changeset.apply_action!(changeset, :insert), user.email, &url(~p"/users/settings/confirm-email/#{&1}") ) socket = socket |> put_flash(:info, "A link to confirm your email change has been sent to the new address.") |> assign_changesets() {:noreply, socket} changeset -> {:noreply, assign(socket, :email_form, to_form(%{changeset | action: :insert}))} end end @impl true def handle_event("update_password", %{"user" => user_params}, socket) do user = socket.assigns.current_scope.user case Accounts.update_user_password(user, user_params) do {:ok, {_updated_user, _}} -> # Redirect to re-authenticate with new password socket = socket |> put_flash(:info, "Password updated successfully. Please log in with your new password.") |> redirect(to: ~p"/users/log-in") {:noreply, socket} {:error, changeset} -> {:noreply, assign(socket, :password_form, to_form(changeset))} end end @impl true def handle_event("revoke_mobile_device", %{"session-id" => session_id}, socket) do case MobileSessions.revoke_session(session_id) do {:ok, _} -> socket = socket |> put_flash(:info, "Mobile device removed successfully.") |> assign_mobile_sessions() {:noreply, socket} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to remove mobile device.")} end end @impl true def handle_event("toggle_device_alerts", %{"session-id" => session_id, "enabled" => enabled}, socket) do enabled_bool = enabled == "true" case MobileSessions.update_alert_preferences(session_id, %{alerts_enabled: enabled_bool}) do {:ok, _} -> message = if enabled_bool, do: "Alerts enabled for device", else: "Alerts disabled for device" socket = socket |> put_flash(:info, message) |> assign_mobile_sessions() {:noreply, socket} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to update alert preferences.")} end end defp assign_changesets(socket) do user = socket.assigns.current_scope.user socket |> assign(:email_form, user |> Accounts.change_user_email() |> to_form()) |> assign(:password_form, user |> Accounts.change_user_password() |> to_form()) end defp assign_credentials(socket) do user = socket.assigns.current_scope.user socket |> assign(:credentials, Accounts.list_user_credentials(user.id)) |> assign(:can_register_passkey, Accounts.passkey_registration_allowed?(user)) end defp assign_mobile_sessions(socket) do user = socket.assigns.current_scope.user assign(socket, :mobile_sessions, MobileSessions.list_user_sessions(user.id)) end @impl true def render(assigns) do ~H"""
<.header> Account Settings <:subtitle>Manage your account email address and password settings
<.form for={@email_form} phx-submit="update_email" id="update_email" class="space-y-4" > <.input field={@email_form[:email]} type="email" label="Email" autocomplete="email" required /> <.button variant="primary" phx-disable-with="Changing...">Change Email
<.form for={@password_form} phx-submit="update_password" id="update_password" class="space-y-4" > <.input field={@password_form[:password]} type="password" label="New password" autocomplete="new-password" required /> <.input field={@password_form[:password_confirmation]} type="password" label="Confirm new password" autocomplete="new-password" required /> <.button variant="primary" phx-disable-with="Changing..."> Save Password

Alert Notification Devices

Manage mobile devices that receive push notifications for alerts

<%= if Enum.empty?(@mobile_sessions) do %>
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-zinc-400" />

No mobile devices registered

Add a mobile device to receive push notifications for alerts

<% else %>
<%= for session <- @mobile_sessions do %>
<.icon name="hero-device-phone-mobile" class="h-5 w-5 text-zinc-400" />

{session.device_name || "Unknown Device"}

{session.device_os} • {session.app_version}

Last used {Calendar.strftime(session.last_used_at, "%B %d, %Y")}

<% end %>
<% end %> <.link navigate={~p"/mobile/qr-login"} class="inline-flex items-center gap-2 rounded-lg bg-zinc-900 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-zinc-700 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-300" > <.icon name="hero-qr-code" class="h-4 w-4" /> Add Mobile Device

Passkeys

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

<%= if !@can_register_passkey do %>

Please confirm your email address before registering a passkey. Check your inbox for the confirmation link.

<% end %>
<%= if Enum.empty?(@credentials) do %>
<.icon name="hero-key" class="mx-auto h-12 w-12 text-zinc-400" />

No passkeys registered

Add a passkey to enable quick, secure sign-in with biometrics

<% else %>
<%= for credential <- @credentials do %>
<.icon name="hero-key" class="h-5 w-5 text-zinc-400" />

{credential.name}

<%= if credential.last_used_at do %>

Last used {Calendar.strftime(credential.last_used_at, "%B %d, %Y")}

<% else %>

Never used

<% end %>
<.link href={~p"/users/credentials/#{credential.id}"} method="delete" data-confirm="Are you sure you want to delete this passkey?" class="text-sm font-semibold text-red-600 hover:text-red-500 dark:text-red-400 dark:hover:text-red-300" > Delete
<% end %>
<% end %> <%= if @can_register_passkey do %> <% end %>
""" end end