diff --git a/assets/vue/components/log-in/LoginForm.vue b/assets/vue/components/log-in/LoginForm.vue index 7fe4545..9e0e76d 100644 --- a/assets/vue/components/log-in/LoginForm.vue +++ b/assets/vue/components/log-in/LoginForm.vue @@ -51,12 +51,12 @@ const submit = () => {
- Forgot your password? - +
diff --git a/assets/vue/components/registration/RegistrationForm.vue b/assets/vue/components/registration/RegistrationForm.vue index 2bf312c..e21ede9 100644 --- a/assets/vue/components/registration/RegistrationForm.vue +++ b/assets/vue/components/registration/RegistrationForm.vue @@ -35,7 +35,7 @@ const passwordField = form.field("password");
- +
import { GalleryVerticalEnd } from "lucide-vue-next"; +import { Link } from "live_vue"; import LoginForm from "@/components/log-in/LoginForm.vue"; const URL = import.meta.env.VITE_PHOENIX_URL; @@ -9,14 +10,14 @@ const URL = import.meta.env.VITE_PHOENIX_URL;
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)