diff --git a/assets/vue/pages/registration/UserRegistration.vue b/assets/vue/pages/registration/UserRegistration.vue
index 14408d1..9d8af2b 100644
--- a/assets/vue/pages/registration/UserRegistration.vue
+++ b/assets/vue/pages/registration/UserRegistration.vue
@@ -1,5 +1,6 @@
+
+
+
+
diff --git a/lib/katana_web/live/forgot-password/components/ForgotPasswordForm.vue b/lib/katana_web/live/forgot-password/components/ForgotPasswordForm.vue
new file mode 100644
index 0000000..c7c2552
--- /dev/null
+++ b/lib/katana_web/live/forgot-password/components/ForgotPasswordForm.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
diff --git a/lib/katana_web/live/forgot-password/forgot_password_live.ex b/lib/katana_web/live/forgot-password/forgot_password_live.ex
new file mode 100644
index 0000000..591a4eb
--- /dev/null
+++ b/lib/katana_web/live/forgot-password/forgot_password_live.ex
@@ -0,0 +1,32 @@
+defmodule KatanaWeb.ForgotPasswordLive do
+ use KatanaWeb, {:live_view, :root}
+
+ alias Katana.Accounts
+
+ def render(assigns) do
+ ~H"""
+ <.vue v-component="ForgotPassword" v-socket={@socket} />
+ """
+ end
+
+ def mount(_params, _session, socket) do
+ {:ok, assign(socket, form: to_form(%{}, as: "user"))}
+ end
+
+ def handle_event("send_email", %{"user" => %{"email" => email}}, socket) do
+ if user = Accounts.get_user_by_email(email) do
+ Accounts.deliver_reset_password_instructions(
+ user,
+ &url(~p"/reset_password/#{&1}")
+ )
+ end
+
+ {:noreply,
+ socket
+ |> put_flash(
+ :info,
+ "If your email is in our system, you will receive instructions to reset your password shortly."
+ )
+ |> redirect(to: ~p"/")}
+ end
+end
diff --git a/lib/katana_web/live/reset-password/ResetPassword.vue b/lib/katana_web/live/reset-password/ResetPassword.vue
new file mode 100644
index 0000000..5ddd66a
--- /dev/null
+++ b/lib/katana_web/live/reset-password/ResetPassword.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/lib/katana_web/live/reset-password/components/ResetPasswordForm.vue b/lib/katana_web/live/reset-password/components/ResetPasswordForm.vue
new file mode 100644
index 0000000..c461a6f
--- /dev/null
+++ b/lib/katana_web/live/reset-password/components/ResetPasswordForm.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
diff --git a/lib/katana_web/live/reset-password/reset_password_live.ex b/lib/katana_web/live/reset-password/reset_password_live.ex
new file mode 100644
index 0000000..9ff3e93
--- /dev/null
+++ b/lib/katana_web/live/reset-password/reset_password_live.ex
@@ -0,0 +1,71 @@
+defmodule KatanaWeb.ResetPasswordLive do
+ use KatanaWeb, {:live_view, :root}
+
+ alias Katana.Accounts
+
+ def render(assigns) do
+ ~H"""
+ <.vue v-component="ResetPassword" form={@form} v-socket={@socket} />
+ """
+ end
+
+ def mount(params, _session, socket) do
+ socket = assign_user_and_token(socket, params)
+
+ source =
+ case socket.assigns do
+ %{user: user} ->
+ Accounts.change_user_password(user)
+
+ _ ->
+ %{}
+ end
+
+ form = to_form(source, as: "user")
+ {:ok, assign(socket, form: form), temporary_assigns: [form: nil]}
+ end
+
+ def handle_event("validate", %{"user" => params}, socket) do
+ changeset =
+ Accounts.change_user_password(socket.assigns.user, params)
+ |> Map.put(:action, :validate)
+
+ {:noreply, reload(socket, changeset)}
+ end
+
+ # Do not log in the user after reset password to avoid a
+ # leaked token giving the user access to the account.
+ def handle_event("reset_password", %{"user" => params}, socket) do
+ case Accounts.reset_user_password(socket.assigns.user, params) do
+ {:ok, _} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Password reset successfully. Please proceed to log in.")
+ |> redirect(to: ~p"/users/log_in")}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply,
+ socket
+ |> put_flash(
+ :error,
+ "Password reset failed. Please try again and if the problem persists, contact support."
+ )
+ |> reload(Map.put(changeset, :action, :insert))}
+ end
+ end
+
+ defp assign_user_and_token(socket, %{"token" => token}) do
+ if user = Accounts.get_user_by_reset_password_token(token) do
+ assign(socket, user: user, token: token)
+ else
+ socket
+ |> put_flash(:error, "Reset password link is invalid or it has expired.")
+ |> redirect(to: ~p"/")
+ end
+ end
+
+ defp reload(socket, changeset) do
+ form = to_form(changeset, as: "user")
+ assign(socket, form: form)
+ end
+end
diff --git a/lib/katana_web/live/reset-password/types/form.ts b/lib/katana_web/live/reset-password/types/form.ts
new file mode 100644
index 0000000..5f67c86
--- /dev/null
+++ b/lib/katana_web/live/reset-password/types/form.ts
@@ -0,0 +1,4 @@
+type ResetPasswordFields = {
+ password: string;
+ password_confirmation: string;
+};
diff --git a/lib/katana_web/router.ex b/lib/katana_web/router.ex
index 7f9aef0..2775cdf 100644
--- a/lib/katana_web/router.ex
+++ b/lib/katana_web/router.ex
@@ -47,8 +47,8 @@ defmodule KatanaWeb.Router do
on_mount: [{KatanaWeb.UserAuth, :redirect_if_user_is_authenticated}] do
live "/users/register", UserRegistrationLive, :new
live "/users/log_in", UserLoginLive, :new
- live "/users/reset_password", UserForgotPasswordLive, :new
- live "/users/reset_password/:token", UserResetPasswordLive, :edit
+ live "/forgot_password", ForgotPasswordLive, :new
+ live "/reset_password/:token", ResetPasswordLive, :edit
end
post "/users/log_in", UserSessionController, :create
diff --git a/test/katana/accounts_test.exs b/test/katana/accounts_test.exs
index efc00ff..a9b0a08 100644
--- a/test/katana/accounts_test.exs
+++ b/test/katana/accounts_test.exs
@@ -415,7 +415,7 @@ defmodule Katana.AccountsTest do
end
end
- describe "deliver_user_reset_password_instructions/2" do
+ describe "deliver_reset_password_instructions/2" do
setup do
%{user: user_fixture()}
end
@@ -423,7 +423,7 @@ defmodule Katana.AccountsTest do
test "sends token through notification", %{user: user} do
token =
extract_user_token(fn url ->
- Accounts.deliver_user_reset_password_instructions(user, url)
+ Accounts.deliver_reset_password_instructions(user, url)
end)
{:ok, token} = Base.url_decode64(token, padding: false)