- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat - Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast - ~11,600 color replacements across ~99 files - Update tests for new color class names
174 lines
5.5 KiB
Elixir
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-wheat-50 dark:bg-wheat-900/20 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-wheat-400" />
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-wheat-800 dark:text-wheat-200">
|
|
Password found in data breaches
|
|
</h3>
|
|
<div class="mt-2 text-sm text-wheat-700 dark:text-wheat-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-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
|
|
>
|
|
Back to log in
|
|
</.link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Layouts.app>
|
|
"""
|
|
end
|
|
end
|