towerops/lib/towerops_web/live/user_reset_password_live.ex

174 lines
5.5 KiB
Elixir

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"""
<Layouts.app flash={@flash}>
<div class="mx-auto max-w-sm">
<div class="text-center">
<.header>
Reset password
<:subtitle>
Enter your new password below.
</:subtitle>
</.header>
</div>
<div class="mt-8">
<.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 %>
<div class="mt-2 rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<div class="flex">
<div class="flex-shrink-0">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-yellow-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Password found in data breaches
</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
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.
</p>
</div>
</div>
</div>
</div>
<% end %>
<.input
field={@form[:password_confirmation]}
type="password"
label="Confirm new password"
autocomplete="new-password"
required
/>
<div class="mt-6">
<.button phx-disable-with="Resetting password..." class="w-full" variant="primary">
Reset password
</.button>
</div>
</.form>
<div class="mt-6 text-center">
<.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
</.link>
</div>
</div>
</div>
</Layouts.app>
"""
end
end