From 4ea23a3b40025c13a7f739cdef1d4a8ed42e68b6 Mon Sep 17 00:00:00 2001 From: Rui Lopes Date: Thu, 14 Aug 2025 23:38:14 +0100 Subject: [PATCH 1/5] feat: reset password pages --- .../forgot-password/ForgotPasswordForm.vue | 67 ++++++++++++++ assets/vue/components/log-in/LoginForm.vue | 6 +- .../registration/RegistrationForm.vue | 2 +- .../reset-password/ResetPasswordForm.vue | 87 +++++++++++++++++++ .../forgot-password/UserForgotPassword.vue | 22 +++++ .../reset-password/UserResetPassword.vue | 26 ++++++ lib/katana/accounts/user.ex | 2 +- .../live/auth/user_forgot_password_live.ex | 30 ++----- .../live/auth/user_reset_password_live.ex | 72 ++++++--------- lib/katana_web/router.ex | 2 +- 10 files changed, 241 insertions(+), 75 deletions(-) create mode 100644 assets/vue/components/forgot-password/ForgotPasswordForm.vue create mode 100644 assets/vue/components/reset-password/ResetPasswordForm.vue create mode 100644 assets/vue/pages/forgot-password/UserForgotPassword.vue create mode 100644 assets/vue/pages/reset-password/UserResetPassword.vue diff --git a/assets/vue/components/forgot-password/ForgotPasswordForm.vue b/assets/vue/components/forgot-password/ForgotPasswordForm.vue new file mode 100644 index 0000000..1d0c153 --- /dev/null +++ b/assets/vue/components/forgot-password/ForgotPasswordForm.vue @@ -0,0 +1,67 @@ + + + diff --git a/assets/vue/components/log-in/LoginForm.vue b/assets/vue/components/log-in/LoginForm.vue index 7fe4545..69054fa 100644 --- a/assets/vue/components/log-in/LoginForm.vue +++ b/assets/vue/components/log-in/LoginForm.vue @@ -51,12 +51,12 @@ const submit = () => {
diff --git a/assets/vue/components/registration/RegistrationForm.vue b/assets/vue/components/registration/RegistrationForm.vue index 90fb3b2..aafd027 100644 --- a/assets/vue/components/registration/RegistrationForm.vue +++ b/assets/vue/components/registration/RegistrationForm.vue @@ -39,7 +39,7 @@ const password = fields["password"];
- + +import { toRef } from "vue"; +import { Form, Link, useLiveForm } from "live_vue"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { FieldError } from "@/components/ui/field-error"; + +type ResetPasswordFields = { + password: string; + password_confirmation: string; +}; + +const props = defineProps<{ + form: Form; +}>(); + +const { submit, form, fields, isSubmitting } = useLiveForm( + toRef(props, "form"), + { + changeEvent: "validate", + submitEvent: "reset_password", + }, +); + +const password = fields["password"]; +const passwordConfirmation = fields["password_confirmation"]; + + + diff --git a/assets/vue/pages/forgot-password/UserForgotPassword.vue b/assets/vue/pages/forgot-password/UserForgotPassword.vue new file mode 100644 index 0000000..8fa10d4 --- /dev/null +++ b/assets/vue/pages/forgot-password/UserForgotPassword.vue @@ -0,0 +1,22 @@ + + + diff --git a/assets/vue/pages/reset-password/UserResetPassword.vue b/assets/vue/pages/reset-password/UserResetPassword.vue new file mode 100644 index 0000000..e6195c7 --- /dev/null +++ b/assets/vue/pages/reset-password/UserResetPassword.vue @@ -0,0 +1,26 @@ + + + diff --git a/lib/katana/accounts/user.ex b/lib/katana/accounts/user.ex index e880af2..a8e25df 100644 --- a/lib/katana/accounts/user.ex +++ b/lib/katana/accounts/user.ex @@ -8,7 +8,7 @@ defmodule Katana.Accounts.User do @registration_fields ~w(name email password)a - @derive {LiveVue.Encoder, except: [:hashed_password, :current_password, :confirmed_at]} + @derive {LiveVue.Encoder, except: [:hashed_password, :confirmed_at]} schema "users" do field :name, :string field :email, :string diff --git a/lib/katana_web/live/auth/user_forgot_password_live.ex b/lib/katana_web/live/auth/user_forgot_password_live.ex index 846ebc8..c09a660 100644 --- a/lib/katana_web/live/auth/user_forgot_password_live.ex +++ b/lib/katana_web/live/auth/user_forgot_password_live.ex @@ -1,29 +1,11 @@ defmodule KatanaWeb.UserForgotPasswordLive do - use KatanaWeb, :live_view + use KatanaWeb, {:live_view, :root} alias Katana.Accounts def render(assigns) do ~H""" - <%!--
- <.header class="text-center"> - Forgot your password? - <:subtitle>We'll send a password reset link to your inbox - - - <.simple_form for={@form} id="reset_password_form" phx-submit="send_email"> - <.input field={@form[:email]} type="email" placeholder="Email" required /> - <:actions> - <.button phx-disable-with="Sending..." class="w-full"> - Send password reset instructions - - - -

- <.link href={~p"/users/register"}>Register - | <.link href={~p"/users/log_in"}>Log in -

-
--%> + <.vue v-component="UserForgotPassword" v-socket={@socket} /> """ end @@ -39,12 +21,12 @@ defmodule KatanaWeb.UserForgotPasswordLive do ) end - info = - "If your email is in our system, you will receive instructions to reset your password shortly." - {:noreply, socket - |> put_flash(:info, info) + |> 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/auth/user_reset_password_live.ex b/lib/katana_web/live/auth/user_reset_password_live.ex index c20c913..b8d1d94 100644 --- a/lib/katana_web/live/auth/user_reset_password_live.ex +++ b/lib/katana_web/live/auth/user_reset_password_live.ex @@ -1,47 +1,18 @@ defmodule KatanaWeb.UserResetPasswordLive do - use KatanaWeb, :live_view + use KatanaWeb, {:live_view, :root} alias Katana.Accounts def render(assigns) do ~H""" - <%!--
- <.header class="text-center">Reset Password - - <.simple_form - for={@form} - id="reset_password_form" - phx-submit="reset_password" - phx-change="validate" - > - <.error :if={@form.errors != []}> - Oops, something went wrong! Please check the errors below. - - - <.input field={@form[:password]} type="password" label="New password" required /> - <.input - field={@form[:password_confirmation]} - type="password" - label="Confirm new password" - required - /> - <:actions> - <.button phx-disable-with="Resetting..." class="w-full">Reset Password - - - -

- <.link href={~p"/users/register"}>Register - | <.link href={~p"/users/log_in"}>Log in -

-
--%> + <.vue v-component="UserResetPassword" form={@form} v-socket={@socket} /> """ end def mount(params, _session, socket) do socket = assign_user_and_token(socket, params) - form_source = + source = case socket.assigns do %{user: user} -> Accounts.change_user_password(user) @@ -50,29 +21,39 @@ defmodule KatanaWeb.UserResetPasswordLive do %{} end - {:ok, assign_form(socket, form_source), temporary_assigns: [form: nil]} + 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" => user_params}, socket) do - case Accounts.reset_user_password(socket.assigns.user, user_params) do + 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.") + |> put_flash(:info, "Password reset successfully. Please proceed to log in.") |> redirect(to: ~p"/users/log_in")} - {:error, changeset} -> - {:noreply, assign_form(socket, Map.put(changeset, :action, :insert))} + {: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 - def handle_event("validate", %{"user" => user_params}, socket) do - changeset = Accounts.change_user_password(socket.assigns.user, user_params) - {:noreply, assign_form(socket, Map.put(changeset, :action, :validate))} - 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) @@ -83,7 +64,8 @@ defmodule KatanaWeb.UserResetPasswordLive do end end - defp assign_form(socket, %{} = source) do - assign(socket, :form, to_form(source, as: "user")) + defp reload(socket, changeset) do + form = to_form(changeset, as: "user") + assign(socket, form: form) end end diff --git a/lib/katana_web/router.ex b/lib/katana_web/router.ex index 7f9aef0..d63b3dd 100644 --- a/lib/katana_web/router.ex +++ b/lib/katana_web/router.ex @@ -47,7 +47,7 @@ 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/forgot_password", UserForgotPasswordLive, :new live "/users/reset_password/:token", UserResetPasswordLive, :edit end From ece0a390938af796957ca1b1cf2941d3bb09b4e8 Mon Sep 17 00:00:00 2001 From: Rui Lopes Date: Sat, 16 Aug 2025 18:06:34 +0100 Subject: [PATCH 2/5] Update assets/vue/components/forgot-password/ForgotPasswordForm.vue Co-authored-by: GuilhermePSF --- .../forgot-password/ForgotPasswordForm.vue | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/assets/vue/components/forgot-password/ForgotPasswordForm.vue b/assets/vue/components/forgot-password/ForgotPasswordForm.vue index 1d0c153..0da1ccf 100644 --- a/assets/vue/components/forgot-password/ForgotPasswordForm.vue +++ b/assets/vue/components/forgot-password/ForgotPasswordForm.vue @@ -48,14 +48,16 @@ const submit = () => { required />
- +
Remember your password? From 360888874ad843e119ffdc9499d0af2812bb1ba0 Mon Sep 17 00:00:00 2001 From: Rui Lopes Date: Sat, 16 Aug 2025 18:12:02 +0100 Subject: [PATCH 3/5] chore: apply suggestions --- assets/vue/pages/forgot-password/UserForgotPassword.vue | 5 +++-- assets/vue/pages/log-in/UserLogin.vue | 5 +++-- assets/vue/pages/registration/UserRegistration.vue | 5 +++-- assets/vue/pages/reset-password/UserResetPassword.vue | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/assets/vue/pages/forgot-password/UserForgotPassword.vue b/assets/vue/pages/forgot-password/UserForgotPassword.vue index 8fa10d4..5774861 100644 --- a/assets/vue/pages/forgot-password/UserForgotPassword.vue +++ b/assets/vue/pages/forgot-password/UserForgotPassword.vue @@ -1,5 +1,6 @@ @@ -7,14 +8,14 @@ import ForgotPasswordForm from "@/components/forgot-password/ForgotPasswordForm.
diff --git a/assets/vue/pages/log-in/UserLogin.vue b/assets/vue/pages/log-in/UserLogin.vue index cf85916..8e3039f 100644 --- a/assets/vue/pages/log-in/UserLogin.vue +++ b/assets/vue/pages/log-in/UserLogin.vue @@ -1,5 +1,6 @@