defmodule ToweropsWeb.UserResetPasswordLive do @moduledoc """ LiveView for resetting user password with HIBP password breach checking. """ use ToweropsWeb, :live_view alias Towerops.Accounts alias Towerops.Accounts.HIBP @impl true def mount(%{"token" => token}, _session, socket) do case Accounts.get_user_by_reset_password_token(token) do nil -> {:ok, socket |> put_flash(:error, t("Reset password link is invalid or has expired.")) |> redirect(to: ~p"/users/log-in")} user -> changeset = Accounts.change_user_password(user) {:ok, socket |> assign(:page_title, t("Reset Password")) |> assign(:form, to_form(changeset)) |> assign(:user, user) |> assign(:password_breach_count, nil)} end end @impl true def handle_params(params, _url, socket) do case Map.get(params, "token", "") do "" -> {:noreply, socket |> put_flash(:error, t("Reset password link is invalid or has expired.")) |> redirect(to: ~p"/users/log-in")} token -> case Accounts.get_user_by_reset_password_token(token) do nil -> {:noreply, socket |> put_flash(:error, t("Reset password link is invalid or has expired.")) |> redirect(to: ~p"/users/log-in")} user -> changeset = Accounts.change_user_password(user) {:noreply, socket |> assign(:page_title, t("Reset Password")) |> assign(:form, to_form(changeset)) |> assign(:user, user) |> assign(:password_breach_count, nil)} end end end @impl true def handle_event("check_password_breach", %{"value" => password}, socket) when is_binary(password) and byte_size(password) > 0 do case HIBP.check_password(password) do {:ok, count} when is_integer(count) -> {:noreply, assign(socket, :password_breach_count, count)} {:ok, :unknown} -> {:noreply, assign(socket, :password_breach_count, nil)} end end def handle_event("check_password_breach", _params, socket) do {:noreply, socket} end def handle_event("validate", %{"user" => user_params}, socket) do changeset = socket.assigns.user |> Accounts.change_user_password(user_params) |> Map.put(:action, :validate) {:noreply, assign(socket, :form, to_form(changeset))} end def handle_event("save", %{"user" => user_params}, socket) do case Accounts.reset_user_password(socket.assigns.user, user_params) do {:ok, {updated_user, _expired_tokens}} -> {:noreply, socket |> put_flash(:info, t("Password reset successfully.")) |> redirect(to: ~p"/users/log-in?email=#{updated_user.email}")} {:error, changeset} -> {:noreply, assign(socket, :form, to_form(changeset))} end end @impl true def render(assigns) do ~H"""
<.header> Reset password <:subtitle> Enter your new password below.
<.form for={@form} id="reset-password-form" phx-change="validate" phx-submit="save"> <.input field={@form[:password]} type="password" label="New password" autocomplete="new-password" phx-blur="check_password_breach" phx-mounted={JS.focus()} required /> <%= if @password_breach_count && @password_breach_count > 0 do %>
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-yellow-400" />

Password found in data breaches

This password has appeared {@password_breach_count} {if @password_breach_count == 1, do: "time", else: "times"} in known data breaches. We strongly recommend choosing a different, more unique password to protect your account.

<% end %> <.input field={@form[:password_confirmation]} type="password" label="Confirm new password" autocomplete="new-password" required />
<.button phx-disable-with="Resetting password..." class="w-full" variant="primary"> Reset password
<.link navigate={~p"/users/log-in"} class="text-sm font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > Back to log in
""" end end